Merge remote-tracking branch 'soapbox/develop' into lexical
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
e703cd5059
|
@ -1,4 +1,4 @@
|
|||
image: node:18
|
||||
image: node:20
|
||||
|
||||
variables:
|
||||
NODE_ENV: test
|
||||
|
|
|
@ -1 +1 @@
|
|||
nodejs 18.14.0
|
||||
nodejs 20.0.0
|
||||
|
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Posts: Support posts filtering on recent Mastodon versions
|
||||
- Reactions: Support custom emoji reactions
|
||||
- Compatbility: Support Mastodon v2 timeline filters.
|
||||
- Compatbility: Preliminary support for Ditto backend.
|
||||
- Posts: Support dislikes on Friendica.
|
||||
- UI: added a character counter to some textareas.
|
||||
|
||||
|
@ -30,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- 18n: fixed Chinese language being detected from the browser.
|
||||
- Conversations: fixed pagination (Mastodon).
|
||||
- Compatibility: fix version parsing for Friendica.
|
||||
- UI: fixed various overflow issues related to long usernames.
|
||||
- UI: fixed display of Markdown code blocks in the reply indicator.
|
||||
|
||||
## [3.2.0] - 2023-02-15
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:18 as build
|
||||
FROM node:20 as build
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:18
|
||||
FROM node:20
|
||||
|
||||
RUN apt-get update &&\
|
||||
apt-get install -y inotify-tools &&\
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildRelationship } from 'soapbox/jest/factory';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { ReducerRecord, EditRecord } from 'soapbox/reducers/account-notes';
|
||||
|
||||
import { normalizeAccount, normalizeRelationship } from '../../normalizers';
|
||||
import { normalizeAccount } from '../../normalizers';
|
||||
import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';
|
||||
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
@ -66,7 +67,7 @@ describe('initAccountNoteModal()', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('relationships', ImmutableMap({ '1': normalizeRelationship({ note: 'hello' }) }));
|
||||
.set('relationships', ImmutableMap({ '1': buildRelationship({ note: 'hello' }) }));
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildRelationship } from 'soapbox/jest/factory';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists';
|
||||
|
||||
import { normalizeAccount, normalizeInstance, normalizeRelationship } from '../../normalizers';
|
||||
import { normalizeAccount, normalizeInstance } from '../../normalizers';
|
||||
import {
|
||||
authorizeFollowRequest,
|
||||
blockAccount,
|
||||
|
@ -1340,7 +1341,7 @@ describe('fetchRelationships()', () => {
|
|||
describe('without newAccountIds', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('relationships', ImmutableMap({ [id]: normalizeRelationship({}) }))
|
||||
.set('relationships', ImmutableMap({ [id]: buildRelationship() }))
|
||||
.set('me', '123');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@ import api, { getLinks } from '../api';
|
|||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedGroups, importFetchedAccounts } from './importer';
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { GroupRole } from 'soapbox/reducers/group-memberships';
|
||||
|
@ -35,10 +34,6 @@ const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
|
|||
const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
|
||||
const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
|
||||
|
||||
const GROUP_DELETE_STATUS_REQUEST = 'GROUP_DELETE_STATUS_REQUEST';
|
||||
const GROUP_DELETE_STATUS_SUCCESS = 'GROUP_DELETE_STATUS_SUCCESS';
|
||||
const GROUP_DELETE_STATUS_FAIL = 'GROUP_DELETE_STATUS_FAIL';
|
||||
|
||||
const GROUP_KICK_REQUEST = 'GROUP_KICK_REQUEST';
|
||||
const GROUP_KICK_SUCCESS = 'GROUP_KICK_SUCCESS';
|
||||
const GROUP_KICK_FAIL = 'GROUP_KICK_FAIL';
|
||||
|
@ -206,36 +201,6 @@ const fetchGroupRelationshipsFail = (error: AxiosError) => ({
|
|||
skipNotFound: true,
|
||||
});
|
||||
|
||||
const groupDeleteStatus = (groupId: string, statusId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(groupDeleteStatusRequest(groupId, statusId));
|
||||
|
||||
return api(getState).delete(`/api/v1/groups/${groupId}/statuses/${statusId}`)
|
||||
.then(() => {
|
||||
dispatch(deleteFromTimelines(statusId));
|
||||
dispatch(groupDeleteStatusSuccess(groupId, statusId));
|
||||
}).catch(err => dispatch(groupDeleteStatusFail(groupId, statusId, err)));
|
||||
};
|
||||
|
||||
const groupDeleteStatusRequest = (groupId: string, statusId: string) => ({
|
||||
type: GROUP_DELETE_STATUS_REQUEST,
|
||||
groupId,
|
||||
statusId,
|
||||
});
|
||||
|
||||
const groupDeleteStatusSuccess = (groupId: string, statusId: string) => ({
|
||||
type: GROUP_DELETE_STATUS_SUCCESS,
|
||||
groupId,
|
||||
statusId,
|
||||
});
|
||||
|
||||
const groupDeleteStatusFail = (groupId: string, statusId: string, error: AxiosError) => ({
|
||||
type: GROUP_DELETE_STATUS_SUCCESS,
|
||||
groupId,
|
||||
statusId,
|
||||
error,
|
||||
});
|
||||
|
||||
const groupKick = (groupId: string, accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(groupKickRequest(groupId, accountId));
|
||||
|
@ -677,9 +642,6 @@ export {
|
|||
GROUP_RELATIONSHIPS_FETCH_REQUEST,
|
||||
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
|
||||
GROUP_RELATIONSHIPS_FETCH_FAIL,
|
||||
GROUP_DELETE_STATUS_REQUEST,
|
||||
GROUP_DELETE_STATUS_SUCCESS,
|
||||
GROUP_DELETE_STATUS_FAIL,
|
||||
GROUP_KICK_REQUEST,
|
||||
GROUP_KICK_SUCCESS,
|
||||
GROUP_KICK_FAIL,
|
||||
|
@ -735,10 +697,6 @@ export {
|
|||
fetchGroupRelationshipsRequest,
|
||||
fetchGroupRelationshipsSuccess,
|
||||
fetchGroupRelationshipsFail,
|
||||
groupDeleteStatus,
|
||||
groupDeleteStatusRequest,
|
||||
groupDeleteStatusSuccess,
|
||||
groupDeleteStatusFail,
|
||||
groupKick,
|
||||
groupKickRequest,
|
||||
groupKickSuccess,
|
||||
|
|
|
@ -74,7 +74,7 @@ const importFetchedGroup = (group: APIEntity) =>
|
|||
importFetchedGroups([group]);
|
||||
|
||||
const importFetchedGroups = (groups: APIEntity[]) => {
|
||||
const entities = filteredArray(groupSchema).catch([]).parse(groups);
|
||||
const entities = filteredArray(groupSchema).parse(groups);
|
||||
return importGroups(entities);
|
||||
};
|
||||
|
||||
|
|
|
@ -142,7 +142,7 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = ()
|
|||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/trash.svg'),
|
||||
heading: intl.formatMessage(messages.deleteStatusHeading),
|
||||
message: intl.formatMessage(messages.deleteStatusPrompt, { acct }),
|
||||
message: intl.formatMessage(messages.deleteStatusPrompt, { acct: <strong className='break-words'>{acct}</strong> }),
|
||||
confirm: intl.formatMessage(messages.deleteStatusConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteStatus(statusId)).then(() => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import api from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedGroups, importFetchedStatuses } from './importer';
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { SearchFilter } from 'soapbox/reducers/search';
|
||||
|
@ -83,10 +83,6 @@ const submitSearch = (filter?: SearchFilter) =>
|
|||
dispatch(importFetchedStatuses(response.data.statuses));
|
||||
}
|
||||
|
||||
if (response.data.groups) {
|
||||
dispatch(importFetchedGroups(response.data.groups));
|
||||
}
|
||||
|
||||
dispatch(fetchSearchSuccess(response.data, value, type));
|
||||
dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
|
@ -143,10 +139,6 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: (
|
|||
dispatch(importFetchedStatuses(data.statuses));
|
||||
}
|
||||
|
||||
if (data.groups) {
|
||||
dispatch(importFetchedGroups(data.groups));
|
||||
}
|
||||
|
||||
dispatch(expandSearchSuccess(data, value, type));
|
||||
dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { type Account, accountSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
import { useRelationships } from './useRelationships';
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
function useRelationships(ids: string[]) {
|
||||
const api = useApi();
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useDeleteEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
function useDeleteGroupStatus(group: Group, statusId: string) {
|
||||
const api = useApi();
|
||||
const { deleteEntity, isSubmitting } = useDeleteEntity(
|
||||
Entities.STATUSES,
|
||||
() => api.delete(`/api/v1/groups/${group.id}/statuses/${statusId}`),
|
||||
);
|
||||
|
||||
return {
|
||||
mutate: deleteEntity,
|
||||
isSubmitting,
|
||||
};
|
||||
}
|
||||
|
||||
export { useDeleteGroupStatus };
|
|
@ -0,0 +1,24 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { type Group, groupSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useGroupRelationship } from './useGroupRelationship';
|
||||
|
||||
function useGroup(groupId: string, refetch = true) {
|
||||
const api = useApi();
|
||||
|
||||
const { entity: group, ...result } = useEntity<Group>(
|
||||
[Entities.GROUPS, groupId],
|
||||
() => api.get(`/api/v1/groups/${groupId}`),
|
||||
{ schema: groupSchema, refetch },
|
||||
);
|
||||
const { entity: relationship } = useGroupRelationship(groupId);
|
||||
|
||||
return {
|
||||
...result,
|
||||
group: group ? { ...group, relationship: relationship || null } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export { useGroup };
|
|
@ -3,7 +3,7 @@ import { useEntities } from 'soapbox/entity-store/hooks';
|
|||
import { GroupMember, groupMemberSchema } from 'soapbox/schemas';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
||||
import { useApi } from '../useApi';
|
||||
import { useApi } from '../../../hooks/useApi';
|
||||
|
||||
function useGroupMembers(groupId: string, role: GroupRoles) {
|
||||
const api = useApi();
|
|
@ -1,10 +1,10 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities, useIncrementEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useDismissEntity, useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { accountSchema } from 'soapbox/schemas';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
||||
import { useGroupRelationship } from './useGroups';
|
||||
import { useGroupRelationship } from './useGroupRelationship';
|
||||
|
||||
import type { ExpandedEntitiesPath } from 'soapbox/entity-store/hooks/types';
|
||||
|
||||
|
@ -14,7 +14,7 @@ function useGroupMembershipRequests(groupId: string) {
|
|||
|
||||
const { entity: relationship } = useGroupRelationship(groupId);
|
||||
|
||||
const { entities, invalidate, ...rest } = useEntities(
|
||||
const { entities, invalidate, fetchEntities, ...rest } = useEntities(
|
||||
path,
|
||||
() => api.get(`/api/v1/groups/${groupId}/membership_requests`),
|
||||
{
|
||||
|
@ -23,13 +23,13 @@ function useGroupMembershipRequests(groupId: string) {
|
|||
},
|
||||
);
|
||||
|
||||
const { incrementEntity: authorize } = useIncrementEntity(path, -1, async (accountId: string) => {
|
||||
const { dismissEntity: authorize } = useDismissEntity(path, async (accountId: string) => {
|
||||
const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`);
|
||||
invalidate();
|
||||
return response;
|
||||
});
|
||||
|
||||
const { incrementEntity: reject } = useIncrementEntity(path, -1, async (accountId: string) => {
|
||||
const { dismissEntity: reject } = useDismissEntity(path, async (accountId: string) => {
|
||||
const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`);
|
||||
invalidate();
|
||||
return response;
|
||||
|
@ -37,6 +37,7 @@ function useGroupMembershipRequests(groupId: string) {
|
|||
|
||||
return {
|
||||
accounts: entities,
|
||||
refetch: fetchEntities,
|
||||
authorize,
|
||||
reject,
|
||||
...rest,
|
|
@ -0,0 +1,32 @@
|
|||
import { useEffect } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { fetchGroupRelationshipsSuccess } from 'soapbox/actions/groups';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useApi, useAppDispatch } from 'soapbox/hooks';
|
||||
import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
function useGroupRelationship(groupId: string) {
|
||||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { entity: groupRelationship, ...result } = useEntity<GroupRelationship>(
|
||||
[Entities.GROUP_RELATIONSHIPS, groupId],
|
||||
() => api.get(`/api/v1/groups/relationships?id[]=${groupId}`),
|
||||
{ schema: z.array(groupRelationshipSchema).transform(arr => arr[0]) },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupRelationship?.id) {
|
||||
dispatch(fetchGroupRelationshipsSuccess([groupRelationship]));
|
||||
}
|
||||
}, [groupRelationship?.id]);
|
||||
|
||||
return {
|
||||
entity: groupRelationship,
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
||||
export { useGroupRelationship };
|
|
@ -0,0 +1,27 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
function useGroupRelationships(groupIds: string[]) {
|
||||
const api = useApi();
|
||||
const q = groupIds.map(id => `id[]=${id}`).join('&');
|
||||
|
||||
const { entities, ...result } = useEntities<GroupRelationship>(
|
||||
[Entities.GROUP_RELATIONSHIPS, ...groupIds],
|
||||
() => api.get(`/api/v1/groups/relationships?${q}`),
|
||||
{ schema: groupRelationshipSchema, enabled: groupIds.length > 0 },
|
||||
);
|
||||
|
||||
const relationships = entities.reduce<Record<string, GroupRelationship>>((map, relationship) => {
|
||||
map[relationship.id] = relationship;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...result,
|
||||
relationships,
|
||||
};
|
||||
}
|
||||
|
||||
export { useGroupRelationships };
|
|
@ -1,11 +1,9 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||
import { groupSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useApi } from '../../useApi';
|
||||
import { useFeatures } from '../../useFeatures';
|
||||
|
||||
import { useGroupRelationships } from './useGroups';
|
||||
import { useGroupRelationships } from './useGroupRelationships';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { useFeatures } from 'soapbox/hooks/useFeatures';
|
||||
import { groupSchema, type Group } from 'soapbox/schemas/group';
|
||||
|
||||
import { useGroupRelationships } from './useGroupRelationships';
|
||||
|
||||
function useGroups(q: string = '') {
|
||||
const api = useApi();
|
||||
const features = useFeatures();
|
||||
|
||||
const { entities, ...result } = useEntities<Group>(
|
||||
[Entities.GROUPS, 'search', q],
|
||||
() => api.get('/api/v1/groups', { params: { q } }),
|
||||
{ enabled: features.groups, schema: groupSchema },
|
||||
);
|
||||
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||
|
||||
const groups = entities.map((group) => ({
|
||||
...group,
|
||||
relationship: relationships[group.id] || null,
|
||||
}));
|
||||
|
||||
return {
|
||||
...result,
|
||||
groups,
|
||||
};
|
||||
}
|
||||
|
||||
export { useGroups };
|
|
@ -1,11 +1,9 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||
import { groupSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useApi } from '../../useApi';
|
||||
import { useFeatures } from '../../useFeatures';
|
||||
|
||||
import { useGroupRelationships } from './useGroups';
|
||||
import { useGroupRelationships } from './useGroupRelationships';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
|
@ -2,9 +2,10 @@ import { Entities } from 'soapbox/entity-store/entities';
|
|||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { Group, groupSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useGroupRelationships } from '../api/groups/useGroups';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFeatures } from '../useFeatures';
|
||||
import { useApi } from '../../../hooks/useApi';
|
||||
import { useFeatures } from '../../../hooks/useFeatures';
|
||||
|
||||
import { useGroupRelationships } from './useGroupRelationships';
|
||||
|
||||
function usePopularGroups() {
|
||||
const api = useApi();
|
|
@ -1,9 +1,7 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { GroupTag, groupTagSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useApi } from '../../useApi';
|
||||
import { useFeatures } from '../../useFeatures';
|
||||
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||
import { type GroupTag, groupTagSchema } from 'soapbox/schemas';
|
||||
|
||||
function usePopularTags() {
|
||||
const api = useApi();
|
|
@ -1,10 +1,9 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { Group, groupSchema } from 'soapbox/schemas';
|
||||
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||
import { type Group, groupSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useGroupRelationships } from '../api/groups/useGroups';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFeatures } from '../useFeatures';
|
||||
import { useGroupRelationships } from './useGroupRelationships';
|
||||
|
||||
function useSuggestedGroups() {
|
||||
const api = useApi();
|
|
@ -1,7 +1,8 @@
|
|||
|
||||
/**
|
||||
* Accounts
|
||||
*/
|
||||
export { useAccount } from './useAccount';
|
||||
export { useAccount } from './accounts/useAccount';
|
||||
|
||||
/**
|
||||
* Groups
|
||||
|
@ -11,22 +12,29 @@ export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest'
|
|||
export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup';
|
||||
export { useDeleteGroup } from './groups/useDeleteGroup';
|
||||
export { useDemoteGroupMember } from './groups/useDemoteGroupMember';
|
||||
export { useGroup, useGroups } from './groups/useGroups';
|
||||
export { useGroup } from './groups/useGroup';
|
||||
export { useGroupLookup } from './groups/useGroupLookup';
|
||||
export { useGroupMedia } from './groups/useGroupMedia';
|
||||
export { useGroupMembers } from './groups/useGroupMembers';
|
||||
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
|
||||
export { useGroupRelationship } from './groups/useGroupRelationship';
|
||||
export { useGroupRelationships } from './groups/useGroupRelationships';
|
||||
export { useGroupSearch } from './groups/useGroupSearch';
|
||||
export { useGroupTag } from './groups/useGroupTag';
|
||||
export { useGroupTags } from './groups/useGroupTags';
|
||||
export { useGroupValidation } from './groups/useGroupValidation';
|
||||
export { useGroups } from './groups/useGroups';
|
||||
export { useGroupsFromTag } from './groups/useGroupsFromTag';
|
||||
export { useJoinGroup } from './groups/useJoinGroup';
|
||||
export { useLeaveGroup } from './groups/useLeaveGroup';
|
||||
export { usePopularGroups } from './groups/usePopularGroups';
|
||||
export { usePopularTags } from './groups/usePopularTags';
|
||||
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
||||
export { useSuggestedGroups } from './groups/useSuggestedGroups';
|
||||
export { useUpdateGroup } from './groups/useUpdateGroup';
|
||||
export { useUpdateGroupTag } from './groups/useUpdateGroupTag';
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
export { useRelationships } from './useRelationships';
|
||||
export { useRelationships } from './accounts/useRelationships';
|
|
@ -51,7 +51,7 @@ const AuthorizeRejectButtons: React.FC<IAuthorizeRejectButtons> = ({ onAuthorize
|
|||
await action();
|
||||
setState(past);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e) console.error(e);
|
||||
}
|
||||
};
|
||||
if (typeof countdown === 'number') {
|
||||
|
|
|
@ -58,6 +58,7 @@ const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
|
|||
key={accountId}
|
||||
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
|
||||
id={accountId}
|
||||
withRelationship={false}
|
||||
/>
|
||||
))}
|
||||
</Widget>
|
||||
|
|
|
@ -93,7 +93,7 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
|||
>
|
||||
{item.icon && <Icon src={item.icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
||||
|
||||
<span className='truncate'>{item.text}</span>
|
||||
<span className='truncate font-medium'>{item.text}</span>
|
||||
|
||||
{item.count ? (
|
||||
<span className='ml-auto h-5 w-5 flex-none'>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useGroupLookup } from 'soapbox/api/hooks';
|
||||
import ColumnLoading from 'soapbox/features/ui/components/column-loading';
|
||||
import { useGroupLookup } from 'soapbox/hooks/api/groups/useGroupLookup';
|
||||
|
||||
import { Layout } from '../ui';
|
||||
|
||||
|
|
|
@ -56,8 +56,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
|
||||
return (
|
||||
<Comp
|
||||
className={clsx({
|
||||
'flex items-center justify-between px-4 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 dark:from-gradient-start/10 dark:to-gradient-end/10': true,
|
||||
className={clsx('flex items-center justify-between overflow-hidden bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 px-4 py-2 first:rounded-t-lg last:rounded-b-lg dark:from-gradient-start/10 dark:to-gradient-end/10', {
|
||||
'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
|
||||
})}
|
||||
{...linkProps}
|
||||
|
@ -71,7 +70,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
</div>
|
||||
|
||||
{onClick ? (
|
||||
<HStack space={1} alignItems='center' className='text-gray-700 dark:text-gray-600'>
|
||||
<HStack space={1} alignItems='center' className='overflow-hidden text-gray-700 dark:text-gray-600'>
|
||||
{children}
|
||||
|
||||
<Icon src={require('@tabler/icons/chevron-right.svg')} className='ml-1 rtl:rotate-180' />
|
||||
|
|
|
@ -4,14 +4,22 @@ import { IntlProvider } from 'react-intl';
|
|||
import { Provider } from 'react-redux';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { normalizePoll } from 'soapbox/normalizers/poll';
|
||||
import { mockStore, render, screen, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { type Poll } from 'soapbox/schemas';
|
||||
|
||||
import { mockStore, render, screen, rootState } from '../../../jest/test-helpers';
|
||||
import PollFooter from '../poll-footer';
|
||||
|
||||
let poll = normalizePoll({
|
||||
id: 1,
|
||||
options: [{ title: 'Apples', votes_count: 0 }],
|
||||
let poll: Poll = {
|
||||
id: '1',
|
||||
options: [{
|
||||
title: 'Apples',
|
||||
votes_count: 0,
|
||||
title_emojified: 'Apples',
|
||||
}, {
|
||||
title: 'Oranges',
|
||||
votes_count: 0,
|
||||
title_emojified: 'Oranges',
|
||||
}],
|
||||
emojis: [],
|
||||
expired: false,
|
||||
expires_at: '2020-03-24T19:33:06.000Z',
|
||||
|
@ -20,7 +28,7 @@ let poll = normalizePoll({
|
|||
votes_count: 0,
|
||||
own_votes: null,
|
||||
voted: false,
|
||||
});
|
||||
};
|
||||
|
||||
describe('<PollFooter />', () => {
|
||||
describe('with "showResults" enabled', () => {
|
||||
|
@ -62,10 +70,10 @@ describe('<PollFooter />', () => {
|
|||
|
||||
describe('when the Poll has not expired', () => {
|
||||
beforeEach(() => {
|
||||
poll = normalizePoll({
|
||||
...poll.toJS(),
|
||||
poll = {
|
||||
...poll,
|
||||
expired: false,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it('renders time remaining', () => {
|
||||
|
@ -77,10 +85,10 @@ describe('<PollFooter />', () => {
|
|||
|
||||
describe('when the Poll has expired', () => {
|
||||
beforeEach(() => {
|
||||
poll = normalizePoll({
|
||||
...poll.toJS(),
|
||||
poll = {
|
||||
...poll,
|
||||
expired: true,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it('renders closed', () => {
|
||||
|
@ -100,10 +108,10 @@ describe('<PollFooter />', () => {
|
|||
|
||||
describe('when the Poll is multiple', () => {
|
||||
beforeEach(() => {
|
||||
poll = normalizePoll({
|
||||
...poll.toJS(),
|
||||
poll = {
|
||||
...poll,
|
||||
multiple: true,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it('renders the Vote button', () => {
|
||||
|
@ -115,10 +123,10 @@ describe('<PollFooter />', () => {
|
|||
|
||||
describe('when the Poll is not multiple', () => {
|
||||
beforeEach(() => {
|
||||
poll = normalizePoll({
|
||||
...poll.toJS(),
|
||||
poll = {
|
||||
...poll,
|
||||
multiple: false,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it('does not render the Vote button', () => {
|
||||
|
|
|
@ -40,21 +40,21 @@ const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX
|
|||
let votesCount = null;
|
||||
|
||||
if (poll.voters_count !== null && poll.voters_count !== undefined) {
|
||||
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
|
||||
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.voters_count }} />;
|
||||
} else {
|
||||
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
|
||||
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.votes_count }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack space={4} data-testid='poll-footer'>
|
||||
{(!showResults && poll?.multiple) && (
|
||||
{(!showResults && poll.multiple) && (
|
||||
<Button onClick={handleVote} theme='primary' block>
|
||||
<FormattedMessage id='poll.vote' defaultMessage='Vote' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<HStack space={1.5} alignItems='center' wrap>
|
||||
{poll.pleroma.get('non_anonymous') && (
|
||||
{poll.pleroma?.non_anonymous && (
|
||||
<>
|
||||
<Tooltip text={intl.formatMessage(messages.nonAnonymous)}>
|
||||
<Text theme='muted' weight='medium'>
|
||||
|
|
|
@ -112,10 +112,13 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
|||
|
||||
const pollVotesCount = poll.voters_count || poll.votes_count;
|
||||
const percent = pollVotesCount === 0 ? 0 : (option.votes_count / pollVotesCount) * 100;
|
||||
const leading = poll.options.filterNot(other => other.title === option.title).every(other => option.votes_count >= other.votes_count);
|
||||
const voted = poll.own_votes?.includes(index);
|
||||
const message = intl.formatMessage(messages.votes, { votes: option.votes_count });
|
||||
|
||||
const leading = poll.options
|
||||
.filter(other => other.title !== option.title)
|
||||
.every(other => option.votes_count >= other.votes_count);
|
||||
|
||||
return (
|
||||
<div key={option.title}>
|
||||
{showResults ? (
|
||||
|
|
|
@ -7,18 +7,20 @@ import { blockAccount } from 'soapbox/actions/accounts';
|
|||
import { launchChat } from 'soapbox/actions/chats';
|
||||
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose';
|
||||
import { editEvent } from 'soapbox/actions/events';
|
||||
import { groupBlock, groupDeleteStatus, groupKick } from 'soapbox/actions/groups';
|
||||
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'soapbox/actions/interactions';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
||||
import { deleteFromTimelines } from 'soapbox/actions/timelines';
|
||||
import { useDeleteGroupStatus } from 'soapbox/api/hooks/groups/useDeleteGroupStatus';
|
||||
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||
import StatusReactionWrapper from 'soapbox/components/status-reaction-wrapper';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLocal, isRemote } from 'soapbox/utils/accounts';
|
||||
import copy from 'soapbox/utils/copy';
|
||||
|
@ -87,16 +89,7 @@ const messages = defineMessages({
|
|||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
replies_disabled_group: { id: 'status.disabled_replies.group_membership', defaultMessage: 'Only group members can reply' },
|
||||
groupModDelete: { id: 'status.group_mod_delete', defaultMessage: 'Delete post from group' },
|
||||
groupModKick: { id: 'status.group_mod_kick', defaultMessage: 'Kick @{name} from group' },
|
||||
groupModBlock: { id: 'status.group_mod_block', defaultMessage: 'Block @{name} from group' },
|
||||
deleteFromGroupHeading: { id: 'confirmations.delete_from_group.heading', defaultMessage: 'Delete from group' },
|
||||
deleteFromGroupMessage: { id: 'confirmations.delete_from_group.message', defaultMessage: 'Are you sure you want to delete @{name}\'s post?' },
|
||||
kickFromGroupHeading: { id: 'confirmations.kick_from_group.heading', defaultMessage: 'Kick group member' },
|
||||
kickFromGroupMessage: { id: 'confirmations.kick_from_group.message', defaultMessage: 'Are you sure you want to kick @{name} from this group?' },
|
||||
kickFromGroupConfirm: { id: 'confirmations.kick_from_group.confirm', defaultMessage: 'Kick' },
|
||||
blockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Block group member' },
|
||||
blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to block @{name} from interacting with this group?' },
|
||||
blockFromGroupConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Block' },
|
||||
});
|
||||
|
||||
interface IStatusActionBar {
|
||||
|
@ -121,6 +114,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
const features = useFeatures();
|
||||
const settings = useSettings();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const deleteGroupStatus = useDeleteGroupStatus(status?.group as Group, status.id);
|
||||
|
||||
const { allowedEmoji } = soapboxConfig;
|
||||
|
||||
|
@ -258,8 +252,8 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/ban.svg'),
|
||||
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> }} />,
|
||||
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />,
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.id)),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
|
@ -313,31 +307,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.deleteHeading),
|
||||
message: intl.formatMessage(messages.deleteFromGroupMessage, { name: account.username }),
|
||||
message: intl.formatMessage(messages.deleteFromGroupMessage, { name: <strong className='break-words'>{account.username}</strong> }),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(groupDeleteStatus((status.group as Group).id, status.id)),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleKickFromGroup: React.EventHandler<React.MouseEvent> = () => {
|
||||
const account = status.account as Account;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.kickFromGroupHeading),
|
||||
message: intl.formatMessage(messages.kickFromGroupMessage, { name: account.username }),
|
||||
confirm: intl.formatMessage(messages.kickFromGroupConfirm),
|
||||
onConfirm: () => dispatch(groupKick((status.group as Group).id, account.id)),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBlockFromGroup: React.EventHandler<React.MouseEvent> = () => {
|
||||
const account = status.account as Account;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.blockFromGroupHeading),
|
||||
message: intl.formatMessage(messages.blockFromGroupMessage, { name: account.username }),
|
||||
confirm: intl.formatMessage(messages.blockFromGroupConfirm),
|
||||
onConfirm: () => dispatch(groupBlock((status.group as Group).id, account.id)),
|
||||
onConfirm: () => {
|
||||
deleteGroupStatus.mutate(status.id, {
|
||||
onSuccess() {
|
||||
dispatch(deleteFromTimelines(status.id));
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
|
@ -362,7 +340,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
menu.push({
|
||||
text: intl.formatMessage(messages.copy),
|
||||
action: handleCopy,
|
||||
icon: require('@tabler/icons/link.svg'),
|
||||
icon: require('@tabler/icons/clipboard-copy.svg'),
|
||||
});
|
||||
|
||||
if (features.embeds && isLocal(account)) {
|
||||
|
@ -466,7 +444,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
menu.push({
|
||||
text: intl.formatMessage(messages.mute, { name: username }),
|
||||
action: handleMuteClick,
|
||||
icon: require('@tabler/icons/circle-x.svg'),
|
||||
icon: require('@tabler/icons/volume-3.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.block, { name: username }),
|
||||
|
@ -480,23 +458,17 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
});
|
||||
}
|
||||
|
||||
if (status.group && groupRelationship?.role && ['admin', 'moderator'].includes(groupRelationship.role)) {
|
||||
if (status.group &&
|
||||
groupRelationship?.role &&
|
||||
[GroupRoles.OWNER].includes(groupRelationship.role) &&
|
||||
!ownAccount
|
||||
) {
|
||||
menu.push(null);
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.groupModDelete),
|
||||
action: handleDeleteFromGroup,
|
||||
icon: require('@tabler/icons/trash.svg'),
|
||||
});
|
||||
// TODO: figure out when an account is not in the group anymore
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.groupModKick, { name: account.get('username') }),
|
||||
action: handleKickFromGroup,
|
||||
icon: require('@tabler/icons/user-minus.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.groupModBlock, { name: account.get('username') }),
|
||||
action: handleBlockFromGroup,
|
||||
icon: require('@tabler/icons/ban.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
|||
<Link
|
||||
key={account.id}
|
||||
to={`/@${account.acct}`}
|
||||
className='reply-mentions__account'
|
||||
className='reply-mentions__account max-w-[200px] truncate align-bottom'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@{isPubkey(account.username) ? account.username.slice(0, 8) : account.username}
|
||||
|
|
|
@ -102,7 +102,7 @@ const Modal = React.forwardRef<HTMLDivElement, IModal>(({
|
|||
'flex-row-reverse': closePosition === 'left',
|
||||
})}
|
||||
>
|
||||
<h3 className='grow text-lg font-bold leading-6 text-gray-900 dark:text-white'>
|
||||
<h3 className='grow truncate text-lg font-bold leading-6 text-gray-900 dark:text-white'>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import GroupCard from 'soapbox/components/group-card';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetGroup } from 'soapbox/selectors';
|
||||
|
||||
interface IGroupContainer {
|
||||
id: string
|
||||
}
|
||||
|
||||
const GroupContainer: React.FC<IGroupContainer> = (props) => {
|
||||
const { id, ...rest } = props;
|
||||
|
||||
const getGroup = useCallback(makeGetGroup(), []);
|
||||
const group = useAppSelector(state => getGroup(state, id));
|
||||
|
||||
if (group) {
|
||||
return <GroupCard group={group} {...rest} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default GroupContainer;
|
|
@ -130,7 +130,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/ban.svg'),
|
||||
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />,
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.acct}</strong> }} />,
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.id)),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
|
@ -215,7 +215,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
const unfollowModal = getSettings(getState()).get('unfollowModal');
|
||||
if (unfollowModal) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.remove_from_followers.message' defaultMessage='Are you sure you want to remove {name} from your followers?' values={{ name: <strong>@{account.acct}</strong> }} />,
|
||||
message: <FormattedMessage id='confirmations.remove_from_followers.message' defaultMessage='Are you sure you want to remove {name} from your followers?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.removeFromFollowersConfirm),
|
||||
onConfirm: () => dispatch(removeFromFollowers(account.id)),
|
||||
}));
|
||||
|
|
|
@ -6,7 +6,6 @@ import type { Card } from 'soapbox/types/entities';
|
|||
/** Map of available provider modules. */
|
||||
const PROVIDERS: Record<string, () => Promise<AdProvider>> = {
|
||||
soapbox: async() => (await import(/* webpackChunkName: "features/ads/soapbox" */'./soapbox-config')).default,
|
||||
rumble: async() => (await import(/* webpackChunkName: "features/ads/rumble" */'./rumble')).default,
|
||||
truth: async() => (await import(/* webpackChunkName: "features/ads/truth" */'./truth')).default,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import axios from 'axios';
|
||||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { normalizeAd, normalizeCard } from 'soapbox/normalizers';
|
||||
|
||||
import type { AdProvider } from '.';
|
||||
|
||||
/** Rumble ad API entity. */
|
||||
interface RumbleAd {
|
||||
type: number
|
||||
impression: string
|
||||
click: string
|
||||
asset: string
|
||||
expires: number
|
||||
}
|
||||
|
||||
/** Response from Rumble ad server. */
|
||||
interface RumbleApiResponse {
|
||||
count: number
|
||||
ads: RumbleAd[]
|
||||
}
|
||||
|
||||
/** Provides ads from Soapbox Config. */
|
||||
const RumbleAdProvider: AdProvider = {
|
||||
getAds: async(getState) => {
|
||||
const state = getState();
|
||||
const settings = getSettings(state);
|
||||
const soapboxConfig = getSoapboxConfig(state);
|
||||
const endpoint = soapboxConfig.extensions.getIn(['ads', 'endpoint']) as string | undefined;
|
||||
|
||||
if (endpoint) {
|
||||
try {
|
||||
const { data } = await axios.get<RumbleApiResponse>(endpoint, {
|
||||
headers: {
|
||||
'Accept-Language': settings.get('locale', '*') as string,
|
||||
},
|
||||
});
|
||||
|
||||
return data.ads.map(item => normalizeAd({
|
||||
impression: item.impression,
|
||||
card: normalizeCard({
|
||||
type: item.type === 1 ? 'link' : 'rich',
|
||||
image: item.asset,
|
||||
url: item.click,
|
||||
}),
|
||||
expires_at: new Date(item.expires * 1000),
|
||||
}));
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
export default RumbleAdProvider;
|
|
@ -1,18 +1,19 @@
|
|||
import axios from 'axios';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { normalizeCard } from 'soapbox/normalizers';
|
||||
import { cardSchema } from 'soapbox/schemas/card';
|
||||
import { filteredArray } from 'soapbox/schemas/utils';
|
||||
|
||||
import type { AdProvider } from '.';
|
||||
import type { Card } from 'soapbox/types/entities';
|
||||
|
||||
/** TruthSocial ad API entity. */
|
||||
interface TruthAd {
|
||||
impression: string
|
||||
card: Card
|
||||
expires_at: string
|
||||
reason: string
|
||||
}
|
||||
const truthAdSchema = z.object({
|
||||
impression: z.string(),
|
||||
card: cardSchema,
|
||||
expires_at: z.string(),
|
||||
reason: z.string().catch(''),
|
||||
});
|
||||
|
||||
/** Provides ads from the TruthSocial API. */
|
||||
const TruthAdProvider: AdProvider = {
|
||||
|
@ -21,16 +22,13 @@ const TruthAdProvider: AdProvider = {
|
|||
const settings = getSettings(state);
|
||||
|
||||
try {
|
||||
const { data } = await axios.get<TruthAd[]>('/api/v2/truth/ads?device=desktop', {
|
||||
const { data } = await axios.get('/api/v2/truth/ads?device=desktop', {
|
||||
headers: {
|
||||
'Accept-Language': settings.get('locale', '*') as string,
|
||||
'Accept-Language': z.string().catch('*').parse(settings.get('locale')),
|
||||
},
|
||||
});
|
||||
|
||||
return data.map(item => ({
|
||||
...item,
|
||||
card: normalizeCard(item.card),
|
||||
}));
|
||||
return filteredArray(truthAdSchema).parse(data);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { normalizeEmojiReaction } from 'soapbox/normalizers/emoji-reaction';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import ChatMessageReaction from '../chat-message-reaction';
|
||||
|
||||
const emojiReaction = normalizeEmojiReaction({
|
||||
const emojiReaction = ({
|
||||
name: '👍',
|
||||
count: 1,
|
||||
me: false,
|
||||
|
@ -56,7 +54,7 @@ describe('<ChatMessageReaction />', () => {
|
|||
|
||||
render(
|
||||
<ChatMessageReaction
|
||||
emojiReaction={normalizeEmojiReaction({
|
||||
emojiReaction={({
|
||||
name: '👍',
|
||||
count: 1,
|
||||
me: true,
|
||||
|
|
|
@ -312,7 +312,7 @@ const ChatMessage = (props: IChatMessage) => {
|
|||
</Stack>
|
||||
</HStack>
|
||||
|
||||
{(chatMessage.emoji_reactions?.size) ? (
|
||||
{(chatMessage.emoji_reactions?.length) ? (
|
||||
<div
|
||||
className={clsx({
|
||||
'space-y-1': true,
|
||||
|
|
|
@ -2,7 +2,8 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
|
||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
import Markup from 'soapbox/components/markup';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { isRtl } from 'soapbox/rtl';
|
||||
|
||||
|
@ -45,8 +46,8 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ className, status, hideActi
|
|||
hideActions={hideActions}
|
||||
/>
|
||||
|
||||
<Text
|
||||
className='status__content break-words'
|
||||
<Markup
|
||||
className='break-words'
|
||||
size='sm'
|
||||
dangerouslySetInnerHTML={{ __html: status.contentHtml }}
|
||||
direction={isRtl(status.search_index) ? 'rtl' : 'ltr'}
|
||||
|
|
|
@ -9,13 +9,11 @@ import IconButton from 'soapbox/components/icon-button';
|
|||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { HStack, Tabs, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import GroupContainer from 'soapbox/containers/group-container';
|
||||
import StatusContainer from 'soapbox/containers/status-container';
|
||||
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account';
|
||||
import PlaceholderGroupCard from 'soapbox/features/placeholder/components/placeholder-group-card';
|
||||
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder-hashtag';
|
||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import type { VirtuosoHandle } from 'react-virtuoso';
|
||||
|
@ -24,7 +22,6 @@ import type { SearchFilter } from 'soapbox/reducers/search';
|
|||
const messages = defineMessages({
|
||||
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
|
||||
statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
|
||||
groups: { id: 'search_results.groups', defaultMessage: 'Groups' },
|
||||
hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
|
||||
});
|
||||
|
||||
|
@ -33,7 +30,6 @@ const SearchResults = () => {
|
|||
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
|
||||
const value = useAppSelector((state) => state.search.submittedValue);
|
||||
const results = useAppSelector((state) => state.search.results);
|
||||
|
@ -66,14 +62,6 @@ const SearchResults = () => {
|
|||
},
|
||||
);
|
||||
|
||||
if (features.groups) items.push(
|
||||
{
|
||||
text: intl.formatMessage(messages.groups),
|
||||
action: () => selectFilter('groups'),
|
||||
name: 'groups',
|
||||
},
|
||||
);
|
||||
|
||||
items.push(
|
||||
{
|
||||
text: intl.formatMessage(messages.hashtags),
|
||||
|
@ -186,31 +174,6 @@ const SearchResults = () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'groups') {
|
||||
hasMore = results.groupsHasMore;
|
||||
loaded = results.groupsLoaded;
|
||||
placeholderComponent = PlaceholderGroupCard;
|
||||
|
||||
if (results.groups && results.groups.size > 0) {
|
||||
searchResults = results.groups.map((groupId: string) => (
|
||||
<GroupContainer id={groupId} />
|
||||
));
|
||||
resultsIds = results.groups;
|
||||
} else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) {
|
||||
searchResults = null;
|
||||
} else if (loaded) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.groups'
|
||||
defaultMessage='There are no groups results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'hashtags') {
|
||||
hasMore = results.hashtagsHasMore;
|
||||
loaded = results.hashtagsLoaded;
|
||||
|
@ -238,11 +201,11 @@ const SearchResults = () => {
|
|||
{filterByAccount ? (
|
||||
<HStack className='mb-4 border-b border-solid border-gray-200 px-2 pb-4 dark:border-gray-800' space={2}>
|
||||
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/x.svg')} onClick={handleUnsetAccount} />
|
||||
<Text>
|
||||
<Text truncate>
|
||||
<FormattedMessage
|
||||
id='search_results.filter_message'
|
||||
defaultMessage='You are searching for posts from @{acct}.'
|
||||
values={{ acct: account }}
|
||||
values={{ acct: <strong className='break-words'>{account}</strong> }}
|
||||
/>
|
||||
</Text>
|
||||
</HStack>
|
||||
|
|
|
@ -124,7 +124,7 @@ const accountToCredentials = (account: Account): AccountCredentials => {
|
|||
discoverable: account.discoverable,
|
||||
bot: account.bot,
|
||||
display_name: account.display_name,
|
||||
note: account.source.get('note'),
|
||||
note: account.source.get('note', ''),
|
||||
locked: account.locked,
|
||||
fields_attributes: [...account.source.get<Iterable<AccountCredentialsField>>('fields', ImmutableList()).toJS()],
|
||||
stranger_notifications: account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']) === true,
|
||||
|
|
|
@ -207,7 +207,7 @@ const FeedCarousel = () => {
|
|||
style={{ width: widthPerAvatar || 'auto' }}
|
||||
key={idx}
|
||||
>
|
||||
<PlaceholderAvatar size={56} withText />
|
||||
<PlaceholderAvatar size={56} withText className='py-3' />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
|
||||
import { buildGroup, buildGroupRelationship } from 'soapbox/jest/factory';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
import { Group } from 'soapbox/types/entities';
|
||||
|
||||
import GroupActionButton from '../group-action-button';
|
||||
|
@ -45,7 +46,7 @@ describe('<GroupActionButton />', () => {
|
|||
beforeEach(() => {
|
||||
group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
member: null,
|
||||
member: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
@ -98,7 +99,7 @@ describe('<GroupActionButton />', () => {
|
|||
relationship: buildGroupRelationship({
|
||||
requested: false,
|
||||
member: true,
|
||||
role: 'owner',
|
||||
role: GroupRoles.OWNER,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
@ -116,7 +117,7 @@ describe('<GroupActionButton />', () => {
|
|||
relationship: buildGroupRelationship({
|
||||
requested: false,
|
||||
member: true,
|
||||
role: 'user',
|
||||
role: GroupRoles.USER,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
|
||||
import { buildGroup } from 'soapbox/jest/factory';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
import { Group } from 'soapbox/types/entities';
|
||||
|
||||
import GroupHeader from '../group-header';
|
||||
|
||||
let group: Group;
|
||||
|
||||
describe('<GroupHeader />', () => {
|
||||
describe('without a group', () => {
|
||||
it('should render the blankslate', () => {
|
||||
render(<GroupHeader group={null} />);
|
||||
expect(screen.getByTestId('group-header-missing')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the Group has been deleted', () => {
|
||||
it('only shows name, header, and avatar', () => {
|
||||
group = buildGroup({ display_name: 'my group', deleted_at: new Date().toISOString() });
|
||||
render(<GroupHeader group={group} />);
|
||||
|
||||
expect(screen.queryAllByTestId('group-header-missing')).toHaveLength(0);
|
||||
expect(screen.queryAllByTestId('group-actions')).toHaveLength(0);
|
||||
expect(screen.queryAllByTestId('group-meta')).toHaveLength(0);
|
||||
expect(screen.getByTestId('group-header-image')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('group-avatar')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('group-name')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a valid Group', () => {
|
||||
it('only shows all fields', () => {
|
||||
group = buildGroup({ display_name: 'my group', deleted_at: null });
|
||||
render(<GroupHeader group={group} />);
|
||||
|
||||
expect(screen.queryAllByTestId('group-header-missing')).toHaveLength(0);
|
||||
expect(screen.getByTestId('group-actions')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('group-meta')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('group-header-image')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('group-avatar')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('group-name')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,320 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildGroup, buildGroupMember, buildGroupRelationship } from 'soapbox/jest/factory';
|
||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
||||
import GroupMemberListItem from '../group-member-list-item';
|
||||
|
||||
describe('<GroupMemberListItem />', () => {
|
||||
describe('account rendering', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember({}, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the users avatar', async () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship(),
|
||||
});
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('group-member-list-item')).toHaveTextContent(groupMember.account.display_name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('role badge', () => {
|
||||
const accountId = '4';
|
||||
const group = buildGroup();
|
||||
|
||||
describe('when the user is an Owner', () => {
|
||||
const groupMember = buildGroupMember({ role: GroupRoles.OWNER }, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the correct badge', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('role-badge')).toHaveTextContent('owner');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is an Admin', () => {
|
||||
const groupMember = buildGroupMember({ role: GroupRoles.ADMIN }, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the correct badge', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('role-badge')).toHaveTextContent('admin');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is an User', () => {
|
||||
const groupMember = buildGroupMember({ role: GroupRoles.USER }, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render no correct badge', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('role-badge')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as a Group owner', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.OWNER,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
|
||||
describe('when the user has role of "user"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember({}, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "canPromoteToAdmin is true', () => {
|
||||
it('should render dropdown with correct Owner actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
await user.click(screen.getByTestId('icon-button'));
|
||||
});
|
||||
|
||||
const dropdownMenu = screen.getByTestId('dropdown-menu');
|
||||
expect(dropdownMenu).toHaveTextContent('Assign admin role');
|
||||
expect(dropdownMenu).toHaveTextContent('Kick @tiger from group');
|
||||
expect(dropdownMenu).toHaveTextContent('Ban from group');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "canPromoteToAdmin is false', () => {
|
||||
it('should prevent promoting user to Admin', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin={false} />);
|
||||
|
||||
await waitFor(async() => {
|
||||
await user.click(screen.getByTestId('icon-button'));
|
||||
await user.click(screen.getByTitle('Assign admin role'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Admin limit reached');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user has role of "admin"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember(
|
||||
{
|
||||
role: GroupRoles.ADMIN,
|
||||
},
|
||||
{
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render dropdown with correct Owner actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
await user.click(screen.getByTestId('icon-button'));
|
||||
});
|
||||
|
||||
const dropdownMenu = screen.getByTestId('dropdown-menu');
|
||||
expect(dropdownMenu).toHaveTextContent('Remove admin role');
|
||||
expect(dropdownMenu).toHaveTextContent('Kick @tiger from group');
|
||||
expect(dropdownMenu).toHaveTextContent('Ban from group');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as a Group admin', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.ADMIN,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
|
||||
describe('when the user has role of "user"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember({}, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render dropdown with correct Admin actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
await user.click(screen.getByTestId('icon-button'));
|
||||
});
|
||||
|
||||
const dropdownMenu = screen.getByTestId('dropdown-menu');
|
||||
expect(dropdownMenu).not.toHaveTextContent('Assign admin role');
|
||||
expect(dropdownMenu).toHaveTextContent('Kick @tiger from group');
|
||||
expect(dropdownMenu).toHaveTextContent('Ban from group');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user has role of "admin"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember(
|
||||
{
|
||||
role: GroupRoles.ADMIN,
|
||||
},
|
||||
{
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render the dropdown', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user has role of "owner"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember(
|
||||
{
|
||||
role: GroupRoles.OWNER,
|
||||
},
|
||||
{
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render the dropdown', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as a Group user', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.USER,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember({}, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render the dropdown', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -17,7 +17,7 @@ describe('<GroupOptionsButton />', () => {
|
|||
requested: false,
|
||||
member: true,
|
||||
blocked_by: true,
|
||||
role: 'user',
|
||||
role: GroupRoles.USER,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
import React from 'react';
|
||||
|
||||
import { buildGroup, buildGroupTag, buildGroupRelationship } from 'soapbox/jest/factory';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
||||
import GroupTagListItem from '../group-tag-list-item';
|
||||
|
||||
describe('<GroupTagListItem />', () => {
|
||||
describe('tag name', () => {
|
||||
const name = 'hello';
|
||||
|
||||
it('should render the tag name', () => {
|
||||
const group = buildGroup();
|
||||
const tag = buildGroupTag({ name });
|
||||
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
|
||||
|
||||
expect(screen.getByTestId('group-tag-list-item')).toHaveTextContent(`#${name}`);
|
||||
});
|
||||
|
||||
describe('when the tag is "visible"', () => {
|
||||
const group = buildGroup();
|
||||
const tag = buildGroupTag({ name, visible: true });
|
||||
|
||||
it('renders the default name', () => {
|
||||
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
|
||||
expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-900');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the tag is not "visible" and user is Owner', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.OWNER,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
const tag = buildGroupTag({
|
||||
name,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
it('renders the subtle name', () => {
|
||||
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
|
||||
expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-400');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the tag is not "visible" and user is Admin or User', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.ADMIN,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
const tag = buildGroupTag({
|
||||
name,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
it('renders the subtle name', () => {
|
||||
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
|
||||
expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-900');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pinning', () => {
|
||||
describe('as an owner', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.OWNER,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
|
||||
describe('when the tag is visible', () => {
|
||||
const tag = buildGroupTag({ visible: true });
|
||||
|
||||
it('renders the pin icon', () => {
|
||||
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
|
||||
expect(screen.getByTestId('pin-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the tag is not visible', () => {
|
||||
const tag = buildGroupTag({ visible: false });
|
||||
|
||||
it('does not render the pin icon', () => {
|
||||
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
|
||||
expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('as a non-owner', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.ADMIN,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
|
||||
describe('when the tag is visible', () => {
|
||||
const tag = buildGroupTag({ visible: true });
|
||||
|
||||
it('does not render the pin icon', () => {
|
||||
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
|
||||
expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the tag is not visible', () => {
|
||||
const tag = buildGroupTag({ visible: false });
|
||||
|
||||
it('does not render the pin icon', () => {
|
||||
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
|
||||
expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -3,11 +3,11 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
|
||||
import { fetchGroupRelationshipsSuccess } from 'soapbox/actions/groups';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/api/hooks';
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/hooks/api';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { GroupKeys } from 'soapbox/queries/groups';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
|
|
@ -34,7 +34,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
|
||||
if (!group) {
|
||||
return (
|
||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
|
||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6' data-testid='group-header-missing'>
|
||||
<div>
|
||||
<div className='relative h-32 w-full bg-gray-200 dark:bg-gray-900/50 md:rounded-t-xl lg:h-48' />
|
||||
</div>
|
||||
|
@ -107,7 +107,10 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='flex h-32 w-full items-center justify-center bg-gray-200 dark:bg-gray-800/30 md:rounded-t-xl lg:h-52'>
|
||||
<div
|
||||
data-testid='group-header-image'
|
||||
className='flex h-32 w-full items-center justify-center bg-gray-200 dark:bg-gray-800/30 md:rounded-t-xl lg:h-52'
|
||||
>
|
||||
{isHeaderMissing ? (
|
||||
<Icon src={require('@tabler/icons/photo-off.svg')} className='h-6 w-6 text-gray-500 dark:text-gray-700' />
|
||||
) : header}
|
||||
|
@ -120,7 +123,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
<div className='relative'>
|
||||
{renderHeader()}
|
||||
|
||||
<div className='absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2'>
|
||||
<div className='absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2' data-testid='group-avatar'>
|
||||
<a href={group.avatar} onClick={handleAvatarClick} target='_blank'>
|
||||
<GroupAvatar
|
||||
group={group}
|
||||
|
@ -136,11 +139,12 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
size='xl'
|
||||
weight='bold'
|
||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
||||
data-testid='group-name'
|
||||
/>
|
||||
|
||||
{!isDeleted && (
|
||||
<>
|
||||
<Stack space={1} alignItems='center'>
|
||||
<Stack data-testid='group-meta' space={1} alignItems='center'>
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
<GroupRelationship group={group} />
|
||||
<GroupPrivacy group={group} />
|
||||
|
@ -154,7 +158,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
/>
|
||||
</Stack>
|
||||
|
||||
<HStack alignItems='center' space={2}>
|
||||
<HStack alignItems='center' space={2} data-testid='group-actions'>
|
||||
<GroupOptionsButton group={group} />
|
||||
<GroupActionButton group={group} />
|
||||
</HStack>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
|
||||
import { groupKick } from 'soapbox/actions/groups';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useAccount, useBlockGroupMember, useDemoteGroupMember, usePromoteGroupMember } from 'soapbox/api/hooks';
|
||||
import Account from 'soapbox/components/account';
|
||||
import DropdownMenu from 'soapbox/components/dropdown-menu/dropdown-menu';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
|
@ -11,7 +12,6 @@ import { deleteEntities } from 'soapbox/entity-store/actions';
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account';
|
||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
import { useAccount, useBlockGroupMember, useDemoteGroupMember, usePromoteGroupMember } from 'soapbox/hooks/api';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
|
@ -180,7 +180,11 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<HStack
|
||||
alignItems='center'
|
||||
justifyContent='between'
|
||||
data-testid='group-member-list-item'
|
||||
>
|
||||
<div className='w-full'>
|
||||
<Account account={member.account} withRelationship={false} />
|
||||
</div>
|
||||
|
@ -188,6 +192,7 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
|||
<HStack alignItems='center' space={2}>
|
||||
{(isMemberOwner || isMemberAdmin) ? (
|
||||
<span
|
||||
data-testid='role-badge'
|
||||
className={
|
||||
clsx('inline-flex items-center rounded px-2 py-1 text-xs font-medium capitalize', {
|
||||
'bg-primary-200 text-primary-500': isMemberOwner,
|
||||
|
|
|
@ -3,10 +3,10 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||
import { useLeaveGroup } from 'soapbox/api/hooks';
|
||||
import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu';
|
||||
import { IconButton } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import { useLeaveGroup } from 'soapbox/hooks/api';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@ import React from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useUpdateGroupTag } from 'soapbox/api/hooks';
|
||||
import { HStack, IconButton, Stack, Text, Tooltip } from 'soapbox/components/ui';
|
||||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { useUpdateGroupTag } from 'soapbox/hooks/api';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
import toast from 'soapbox/toast';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
|
@ -102,6 +102,7 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
|
|||
require('@tabler/icons/pin.svg')
|
||||
}
|
||||
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
|
||||
data-testid='pin-icon'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -123,13 +124,18 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<HStack
|
||||
alignItems='center'
|
||||
justifyContent='between'
|
||||
data-testid='group-tag-list-item'
|
||||
>
|
||||
<Link to={`/group/${group.slug}/tag/${tag.id}`} className='group grow'>
|
||||
<Stack>
|
||||
<Text
|
||||
weight='bold'
|
||||
theme={(tag.visible || !isOwner) ? 'default' : 'subtle'}
|
||||
className='group-hover:underline'
|
||||
data-testid='group-tag-name'
|
||||
>
|
||||
#{tag.name}
|
||||
</Text>
|
||||
|
@ -137,7 +143,7 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
|
|||
{intl.formatMessage(messages.total)}:
|
||||
{' '}
|
||||
<Text size='sm' theme='inherit' weight='semibold' tag='span'>
|
||||
{shortNumberFormat(tag.groups)}
|
||||
{shortNumberFormat(tag.uses)}
|
||||
</Text>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { useGroup, useUpdateGroup } from 'soapbox/api/hooks';
|
||||
import { Button, Column, Form, FormActions, FormGroup, Icon, Input, Spinner, Textarea } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useInstance } from 'soapbox/hooks';
|
||||
import { useGroup, useUpdateGroup } from 'soapbox/hooks/api';
|
||||
import { useImageField, useTextField } from 'soapbox/hooks/forms';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts';
|
||||
|
|
|
@ -2,11 +2,11 @@ import React, { useCallback, useEffect } from 'react';
|
|||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchGroupBlocks, groupUnblock } from 'soapbox/actions/groups';
|
||||
import { useGroup } from 'soapbox/api/hooks';
|
||||
import Account from 'soapbox/components/account';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Button, Column, HStack, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { useGroup } from 'soapbox/hooks/api';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@ import React from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useGroup, useGroupMedia } from 'soapbox/api/hooks';
|
||||
import LoadMore from 'soapbox/components/load-more';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { useGroup, useGroupMedia } from 'soapbox/hooks/api';
|
||||
|
||||
import MediaItem from '../account-gallery/components/media-item';
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useGroup, useGroupMembers, useGroupMembershipRequests } from 'soapbox/api/hooks';
|
||||
import { PendingItemsRow } from 'soapbox/components/pending-items-row';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { useFeatures } from 'soapbox/hooks';
|
||||
import { useGroup } from 'soapbox/hooks/api';
|
||||
import { useGroupMembershipRequests } from 'soapbox/hooks/api/groups/useGroupMembershipRequests';
|
||||
import { useGroupMembers } from 'soapbox/hooks/api/useGroupMembers';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
||||
import PlaceholderAccount from '../placeholder/components/placeholder-account';
|
||||
|
@ -15,6 +13,7 @@ import GroupMemberListItem from './components/group-member-list-item';
|
|||
|
||||
import type { Group } from 'soapbox/types/entities';
|
||||
|
||||
|
||||
interface IGroupMembers {
|
||||
params: { groupId: string }
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useGroup, useGroupMembers, useGroupMembershipRequests } from 'soapbox/api/hooks';
|
||||
import Account from 'soapbox/components/account';
|
||||
import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column, HStack, Spinner } from 'soapbox/components/ui';
|
||||
import { useGroup, useGroupMembershipRequests } from 'soapbox/hooks/api';
|
||||
import { useGroupMembers } from 'soapbox/hooks/api/useGroupMembers';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
|
@ -59,7 +59,7 @@ const GroupMembershipRequests: React.FC<IGroupMembershipRequests> = ({ params })
|
|||
|
||||
const { group } = useGroup(id);
|
||||
|
||||
const { accounts, authorize, reject, isLoading } = useGroupMembershipRequests(id);
|
||||
const { accounts, authorize, reject, refetch, isLoading } = useGroupMembershipRequests(id);
|
||||
const { invalidate } = useGroupMembers(id, GroupRoles.USER);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -81,19 +81,35 @@ const GroupMembershipRequests: React.FC<IGroupMembershipRequests> = ({ params })
|
|||
}
|
||||
|
||||
async function handleAuthorize(account: AccountEntity) {
|
||||
try {
|
||||
await authorize(account.id);
|
||||
} catch (_e) {
|
||||
toast.error(intl.formatMessage(messages.authorizeFail, { name: account.username }));
|
||||
return authorize(account.id)
|
||||
.then(() => Promise.resolve())
|
||||
.catch((error: AxiosError) => {
|
||||
refetch();
|
||||
|
||||
let message = intl.formatMessage(messages.authorizeFail, { name: account.username });
|
||||
if (error.response?.status === 409) {
|
||||
message = (error.response?.data as any).error;
|
||||
}
|
||||
toast.error(message);
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
|
||||
async function handleReject(account: AccountEntity) {
|
||||
try {
|
||||
await reject(account.id);
|
||||
} catch (_e) {
|
||||
toast.error(intl.formatMessage(messages.rejectFail, { name: account.username }));
|
||||
return reject(account.id)
|
||||
.then(() => Promise.resolve())
|
||||
.catch((error: AxiosError) => {
|
||||
refetch();
|
||||
|
||||
let message = intl.formatMessage(messages.rejectFail, { name: account.username });
|
||||
if (error.response?.status === 409) {
|
||||
message = (error.response?.data as any).error;
|
||||
}
|
||||
toast.error(message);
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -2,9 +2,9 @@ import React, { useEffect } from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { expandGroupTimelineFromTag } from 'soapbox/actions/timelines';
|
||||
import { useGroup, useGroupTag } from 'soapbox/api/hooks';
|
||||
import { Column, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { useGroup, useGroupTag } from 'soapbox/hooks/api';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useGroupTags } from 'soapbox/api/hooks';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useGroupTags } from 'soapbox/hooks/api';
|
||||
import { useGroup } from 'soapbox/queries/groups';
|
||||
|
||||
import PlaceholderAccount from '../placeholder/components/placeholder-account';
|
||||
|
|
|
@ -6,10 +6,10 @@ import { Link } from 'react-router-dom';
|
|||
import { groupCompose, setGroupTimelineVisible, uploadCompose } from 'soapbox/actions/compose';
|
||||
import { connectGroupStream } from 'soapbox/actions/streaming';
|
||||
import { expandGroupTimeline } from 'soapbox/actions/timelines';
|
||||
import { useGroup } from 'soapbox/api/hooks';
|
||||
import { Avatar, HStack, Icon, Stack, Text, Toggle } from 'soapbox/components/ui';
|
||||
import ComposeForm from 'soapbox/features/compose/components/compose-form';
|
||||
import { useAppDispatch, useAppSelector, useDraggedFiles, useOwnAccount } from 'soapbox/hooks';
|
||||
import { useGroup } from 'soapbox/hooks/api';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useDeleteGroup, useGroup } from 'soapbox/api/hooks';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useBackend, useGroupsPath } from 'soapbox/hooks';
|
||||
import { useDeleteGroup, useGroup } from 'soapbox/hooks/api';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
import toast from 'soapbox/toast';
|
||||
import { TRUTHSOCIAL } from 'soapbox/utils/features';
|
||||
|
@ -16,16 +16,16 @@ import ColumnForbidden from '../ui/components/column-forbidden';
|
|||
type RouteParams = { groupId: string };
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.manage_group', defaultMessage: 'Manage group' },
|
||||
editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit group' },
|
||||
heading: { id: 'column.manage_group', defaultMessage: 'Manage Group' },
|
||||
editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit Group' },
|
||||
pendingRequests: { id: 'manage_group.pending_requests', defaultMessage: 'Pending Requests' },
|
||||
blockedMembers: { id: 'manage_group.blocked_members', defaultMessage: 'Banned Members' },
|
||||
deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete group' },
|
||||
deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete Group' },
|
||||
deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' },
|
||||
deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete group' },
|
||||
deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete Group' },
|
||||
deleteMessage: { id: 'confirmations.delete_group.message', defaultMessage: 'Are you sure you want to delete this group? This is a permanent action that cannot be undone.' },
|
||||
members: { id: 'group.tabs.members', defaultMessage: 'Members' },
|
||||
other: { id: 'settings.other', defaultMessage: 'Other options' },
|
||||
other: { id: 'settings.other', defaultMessage: 'Other Options' },
|
||||
deleteSuccess: { id: 'group.delete.success', defaultMessage: 'Group successfully deleted' },
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { usePopularGroups } from 'soapbox/api/hooks';
|
||||
import Link from 'soapbox/components/link';
|
||||
import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover';
|
||||
import { usePopularGroups } from 'soapbox/hooks/api/usePopularGroups';
|
||||
|
||||
import GroupGridItem from './group-grid-item';
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { usePopularTags } from 'soapbox/api/hooks';
|
||||
import Link from 'soapbox/components/link';
|
||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import { usePopularTags } from 'soapbox/hooks/api';
|
||||
|
||||
import TagListItem from './tag-list-item';
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ import React, { useCallback, useState } from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
|
||||
import { useGroupSearch } from 'soapbox/api/hooks';
|
||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useGroupSearch } from 'soapbox/hooks/api';
|
||||
|
||||
import GroupGridItem from '../group-grid-item';
|
||||
import GroupListItem from '../group-list-item';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useGroupSearch } from 'soapbox/api/hooks';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import PlaceholderGroupSearch from 'soapbox/features/placeholder/components/placeholder-group-search';
|
||||
import { useDebounce, useOwnAccount } from 'soapbox/hooks';
|
||||
import { useGroupSearch } from 'soapbox/hooks/api';
|
||||
import { saveGroupSearch } from 'soapbox/utils/groups';
|
||||
|
||||
import Blankslate from './blankslate';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useSuggestedGroups } from 'soapbox/api/hooks';
|
||||
import Link from 'soapbox/components/link';
|
||||
import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover';
|
||||
import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups';
|
||||
|
||||
import GroupGridItem from './group-grid-item';
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useGroups } from 'soapbox/api/hooks';
|
||||
import GroupCard from 'soapbox/components/group-card';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Button, Input, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useDebounce, useFeatures } from 'soapbox/hooks';
|
||||
import { useGroups } from 'soapbox/hooks/api';
|
||||
import { PERMISSION_CREATE_GROUPS, hasPermission } from 'soapbox/utils/permissions';
|
||||
|
||||
import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card';
|
||||
|
|
|
@ -3,8 +3,8 @@ import React, { useCallback, useState } from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
|
||||
import { usePopularGroups } from 'soapbox/api/hooks';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { usePopularGroups } from 'soapbox/hooks/api/usePopularGroups';
|
||||
|
||||
import GroupGridItem from './components/discover/group-grid-item';
|
||||
import GroupListItem from './components/discover/group-list-item';
|
||||
|
|
|
@ -3,8 +3,8 @@ import React, { useCallback, useState } from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
|
||||
import { useSuggestedGroups } from 'soapbox/api/hooks';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups';
|
||||
|
||||
import GroupGridItem from './components/discover/group-grid-item';
|
||||
import GroupListItem from './components/discover/group-list-item';
|
||||
|
|
|
@ -2,8 +2,8 @@ import clsx from 'clsx';
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
|
||||
import { useGroupTag, useGroupsFromTag } from 'soapbox/api/hooks';
|
||||
import { Column, HStack, Icon } from 'soapbox/components/ui';
|
||||
import { useGroupTag, useGroupsFromTag } from 'soapbox/hooks/api';
|
||||
|
||||
import GroupGridItem from './components/discover/group-grid-item';
|
||||
import GroupListItem from './components/discover/group-list-item';
|
||||
|
|
|
@ -3,8 +3,8 @@ import React from 'react';
|
|||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import { usePopularTags } from 'soapbox/api/hooks';
|
||||
import { Column, Text } from 'soapbox/components/ui';
|
||||
import { usePopularTags } from 'soapbox/hooks/api';
|
||||
|
||||
import TagListItem from './components/discover/tag-list-item';
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
|
@ -5,10 +6,11 @@ import { Stack } from 'soapbox/components/ui';
|
|||
interface IPlaceholderAvatar {
|
||||
size: number
|
||||
withText?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** Fake avatar to display while data is loading. */
|
||||
const PlaceholderAvatar: React.FC<IPlaceholderAvatar> = ({ size, withText = false }) => {
|
||||
const PlaceholderAvatar: React.FC<IPlaceholderAvatar> = ({ size, withText = false, className }) => {
|
||||
const style = React.useMemo(() => {
|
||||
if (!size) {
|
||||
return {};
|
||||
|
@ -21,7 +23,10 @@ const PlaceholderAvatar: React.FC<IPlaceholderAvatar> = ({ size, withText = fals
|
|||
}, [size]);
|
||||
|
||||
return (
|
||||
<Stack space={2} className='animate-pulse py-3 text-center'>
|
||||
<Stack
|
||||
space={2}
|
||||
className={clsx('animate-pulse text-center', className)}
|
||||
>
|
||||
<div
|
||||
className='mx-auto block rounded-full bg-primary-50 dark:bg-primary-800'
|
||||
style={style}
|
||||
|
|
|
@ -8,11 +8,11 @@ import PlaceholderDisplayName from './placeholder-display-name';
|
|||
import PlaceholderStatusContent from './placeholder-status-content';
|
||||
|
||||
interface IPlaceholderStatus {
|
||||
variant?: 'rounded' | 'slim'
|
||||
variant?: 'rounded' | 'slim' | 'default'
|
||||
}
|
||||
|
||||
/** Fake status to display while data is loading. */
|
||||
const PlaceholderStatus: React.FC<IPlaceholderStatus> = ({ variant = 'rounded' }) => (
|
||||
const PlaceholderStatus: React.FC<IPlaceholderStatus> = ({ variant }) => (
|
||||
<div
|
||||
className={clsx({
|
||||
'status-placeholder bg-white dark:bg-primary-900': true,
|
||||
|
|
|
@ -74,7 +74,7 @@ const Settings = () => {
|
|||
<CardBody>
|
||||
<List>
|
||||
<ListItem label={intl.formatMessage(messages.editProfile)} onClick={navigateToEditProfile}>
|
||||
<span>{displayName}</span>
|
||||
<span className='max-w-full truncate'>{displayName}</span>
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardBody>
|
||||
|
|
|
@ -125,7 +125,6 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
<Account
|
||||
key={account.id}
|
||||
account={account}
|
||||
timestamp={actualStatus.created_at}
|
||||
avatarSize={42}
|
||||
hideActions
|
||||
approvalStatus={actualStatus.approval_status}
|
||||
|
|
|
@ -230,7 +230,7 @@ const InteractionCounter: React.FC<IInteractionCounter> = ({ count, onClick, chi
|
|||
}
|
||||
>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Text theme='primary' weight='bold'>
|
||||
<Text weight='bold'>
|
||||
{shortNumberFormat(count)}
|
||||
</Text>
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ const ThreadStatus: React.FC<IThreadStatus> = (props): JSX.Element => {
|
|||
// @ts-ignore FIXME
|
||||
<StatusContainer {...props} showGroup={false} />
|
||||
) : (
|
||||
<PlaceholderStatus variant='slim' />
|
||||
<PlaceholderStatus variant='default' />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
// import { Map as ImmutableMap } from 'immutable';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import { normalizeAccount, normalizeRelationship } from '../../../../normalizers';
|
||||
import { buildRelationship } from 'soapbox/jest/factory';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
|
||||
import SubscribeButton from '../subscription-button';
|
||||
|
||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
|
@ -19,162 +20,10 @@ describe('<SubscribeButton />', () => {
|
|||
|
||||
describe('with "accountNotifies" disabled', () => {
|
||||
it('renders nothing', () => {
|
||||
const account = normalizeAccount({ ...justin, relationship: normalizeRelationship({ following: true }) }) as ReducerAccount;
|
||||
const account = normalizeAccount({ ...justin, relationship: buildRelationship({ following: true }) }) as ReducerAccount;
|
||||
|
||||
render(<SubscribeButton account={account} />, undefined, store);
|
||||
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// describe('with "accountNotifies" enabled', () => {
|
||||
// beforeEach(() => {
|
||||
// store = {
|
||||
// ...store,
|
||||
// instance: normalizeInstance({
|
||||
// version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
||||
// software: 'TRUTHSOCIAL',
|
||||
// pleroma: ImmutableMap({}),
|
||||
// }),
|
||||
// };
|
||||
// });
|
||||
|
||||
// describe('when the relationship is requested', () => {
|
||||
// beforeEach(() => {
|
||||
// account = normalizeAccount({ ...account, relationship: normalizeRelationship({ requested: true }) });
|
||||
|
||||
// store = {
|
||||
// ...store,
|
||||
// accounts: ImmutableMap({
|
||||
// '1': account,
|
||||
// }),
|
||||
// };
|
||||
// });
|
||||
|
||||
// it('renders the button', () => {
|
||||
// render(<SubscribeButton account={account} />, null, store);
|
||||
// expect(screen.getByTestId('icon-button')).toBeInTheDocument();
|
||||
// });
|
||||
|
||||
// describe('when the user "isSubscribed"', () => {
|
||||
// beforeEach(() => {
|
||||
// account = normalizeAccount({
|
||||
// ...account,
|
||||
// relationship: normalizeRelationship({ requested: true, notifying: true }),
|
||||
// });
|
||||
|
||||
// store = {
|
||||
// ...store,
|
||||
// accounts: ImmutableMap({
|
||||
// '1': account,
|
||||
// }),
|
||||
// };
|
||||
// });
|
||||
|
||||
// it('renders the unsubscribe button', () => {
|
||||
// render(<SubscribeButton account={account} />, null, store);
|
||||
// expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('when the user is not "isSubscribed"', () => {
|
||||
// beforeEach(() => {
|
||||
// account = normalizeAccount({
|
||||
// ...account,
|
||||
// relationship: normalizeRelationship({ requested: true, notifying: false }),
|
||||
// });
|
||||
|
||||
// store = {
|
||||
// ...store,
|
||||
// accounts: ImmutableMap({
|
||||
// '1': account,
|
||||
// }),
|
||||
// };
|
||||
// });
|
||||
|
||||
// it('renders the unsubscribe button', () => {
|
||||
// render(<SubscribeButton account={account} />, null, store);
|
||||
// expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('when the user is not following the account', () => {
|
||||
// beforeEach(() => {
|
||||
// account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: false }) });
|
||||
|
||||
// store = {
|
||||
// ...store,
|
||||
// accounts: ImmutableMap({
|
||||
// '1': account,
|
||||
// }),
|
||||
// };
|
||||
// });
|
||||
|
||||
// it('renders nothing', () => {
|
||||
// render(<SubscribeButton account={account} />, null, store);
|
||||
// expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('when the user is following the account', () => {
|
||||
// beforeEach(() => {
|
||||
// account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: true }) });
|
||||
|
||||
// store = {
|
||||
// ...store,
|
||||
// accounts: ImmutableMap({
|
||||
// '1': account,
|
||||
// }),
|
||||
// };
|
||||
// });
|
||||
|
||||
// it('renders the button', () => {
|
||||
// render(<SubscribeButton account={account} />, null, store);
|
||||
// expect(screen.getByTestId('icon-button')).toBeInTheDocument();
|
||||
// });
|
||||
|
||||
// describe('when the user "isSubscribed"', () => {
|
||||
// beforeEach(() => {
|
||||
// account = normalizeAccount({
|
||||
// ...account,
|
||||
// relationship: normalizeRelationship({ requested: true, notifying: true }),
|
||||
// });
|
||||
|
||||
// store = {
|
||||
// ...store,
|
||||
// accounts: ImmutableMap({
|
||||
// '1': account,
|
||||
// }),
|
||||
// };
|
||||
// });
|
||||
|
||||
// it('renders the unsubscribe button', () => {
|
||||
// render(<SubscribeButton account={account} />, null, store);
|
||||
// expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('when the user is not "isSubscribed"', () => {
|
||||
// beforeEach(() => {
|
||||
// account = normalizeAccount({
|
||||
// ...account,
|
||||
// relationship: normalizeRelationship({ requested: true, notifying: false }),
|
||||
// });
|
||||
|
||||
// store = {
|
||||
// ...store,
|
||||
// accounts: ImmutableMap({
|
||||
// '1': account,
|
||||
// }),
|
||||
// };
|
||||
// });
|
||||
|
||||
// it('renders the unsubscribe button', () => {
|
||||
// render(<SubscribeButton account={account} />, null, store);
|
||||
// expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
|
|
|
@ -4,9 +4,9 @@ import { useLocation, useRouteMatch } from 'react-router-dom';
|
|||
|
||||
import { groupComposeModal } from 'soapbox/actions/compose';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useGroupLookup } from 'soapbox/api/hooks';
|
||||
import { Avatar, Button, HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { useGroupLookup } from 'soapbox/hooks/api/groups/useGroupLookup';
|
||||
|
||||
const ComposeButton = () => {
|
||||
const location = useLocation();
|
||||
|
@ -25,7 +25,6 @@ const HomeComposeButton = () => {
|
|||
return (
|
||||
<Button
|
||||
theme='accent'
|
||||
icon={require('@tabler/icons/pencil-plus.svg')}
|
||||
size='lg'
|
||||
onClick={onOpenCompose}
|
||||
block
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue