From 7fffe59fb9d3ca93ac7b5a164b5aba38e23592b1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 Mar 2023 17:19:42 -0500 Subject: [PATCH 01/34] Allow "owner" permissions on group pages --- .../features/group/components/group-action-button.tsx | 2 +- app/soapbox/features/group/group-blocked-members.tsx | 2 +- app/soapbox/features/group/group-membership-requests.tsx | 2 +- app/soapbox/features/group/manage-group.tsx | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/group/components/group-action-button.tsx b/app/soapbox/features/group/components/group-action-button.tsx index 32ff557af1..64824866a5 100644 --- a/app/soapbox/features/group/components/group-action-button.tsx +++ b/app/soapbox/features/group/components/group-action-button.tsx @@ -27,7 +27,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { const isRequested = group.relationship?.requested; const isNonMember = !group.relationship?.member && !isRequested; - const isAdmin = group.relationship?.role === 'admin'; + const isAdmin = group.relationship?.role === 'owner'; const isBlocked = group.relationship?.blocked_by; const onJoinGroup = () => joinGroup.mutate(group); diff --git a/app/soapbox/features/group/group-blocked-members.tsx b/app/soapbox/features/group/group-blocked-members.tsx index 7af25f99c5..aae717c12d 100644 --- a/app/soapbox/features/group/group-blocked-members.tsx +++ b/app/soapbox/features/group/group-blocked-members.tsx @@ -81,7 +81,7 @@ const GroupBlockedMembers: React.FC = ({ params }) => { ); } - if (!group.relationship.role || !['admin', 'moderator'].includes(group.relationship.role)) { + if (!group.relationship.role || !['owner', 'admin', 'moderator'].includes(group.relationship.role)) { return (); } diff --git a/app/soapbox/features/group/group-membership-requests.tsx b/app/soapbox/features/group/group-membership-requests.tsx index 8cf0fdfd7b..13f0925aca 100644 --- a/app/soapbox/features/group/group-membership-requests.tsx +++ b/app/soapbox/features/group/group-membership-requests.tsx @@ -96,7 +96,7 @@ const GroupMembershipRequests: React.FC = ({ params }) ); } - if (!group.relationship.role || !['admin', 'moderator'].includes(group.relationship.role)) { + if (!group.relationship.role || !['owner', 'admin', 'moderator'].includes(group.relationship.role)) { return (); } diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index 918aea8741..1ebdc7848c 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -50,7 +50,7 @@ const ManageGroup: React.FC = ({ params }) => { ); } - if (!group.relationship.role || !['admin', 'moderator'].includes(group.relationship.role)) { + if (!group.relationship.role || !['owner', 'admin', 'moderator'].includes(group.relationship.role)) { return (); } @@ -72,7 +72,7 @@ const ManageGroup: React.FC = ({ params }) => { return ( - {group.relationship.role === 'admin' && ( + {group.relationship.role === 'owner' && ( @@ -83,7 +83,7 @@ const ManageGroup: React.FC = ({ params }) => { - {group.relationship.role === 'admin' && ( + {group.relationship.role === 'owner' && ( From 5871abd786f6ccc7bd4b4ea97efe643491fab804 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 Mar 2023 17:59:37 -0500 Subject: [PATCH 02/34] Make "Manage Group" use the EntityStore --- app/soapbox/features/group/manage-group.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index 1ebdc7848c..e7dea7f20b 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -1,13 +1,12 @@ -import React, { useCallback, useEffect } from 'react'; +import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; -import { deleteGroup, editGroup, fetchGroup } from 'soapbox/actions/groups'; +import { deleteGroup, editGroup } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; import List, { ListItem } from 'soapbox/components/list'; import { CardBody, Column, Spinner } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; -import { makeGetGroup } from 'soapbox/selectors'; +import { useAppDispatch, useGroup } from 'soapbox/hooks'; import ColumnForbidden from '../ui/components/column-forbidden'; @@ -29,18 +28,12 @@ interface IManageGroup { } const ManageGroup: React.FC = ({ params }) => { - const history = useHistory(); + const { id } = params; const intl = useIntl(); + const history = useHistory(); const dispatch = useAppDispatch(); - const id = params?.id || ''; - - const getGroup = useCallback(makeGetGroup(), []); - const group = useAppSelector(state => getGroup(state, id)); - - useEffect(() => { - if (!group) dispatch(fetchGroup(id)); - }, [id]); + const { group } = useGroup(id); if (!group || !group.relationship) { return ( From 1518e88904338f92062fb6d7c2b662ae3981bb26 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 Mar 2023 18:37:50 -0500 Subject: [PATCH 03/34] Retrofit old Group actions to EntityStore --- app/soapbox/actions/groups.ts | 3 ++- app/soapbox/actions/importer/index.ts | 25 ++++++++++--------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts index b97d52f206..d78e7f5d8c 100644 --- a/app/soapbox/actions/groups.ts +++ b/app/soapbox/actions/groups.ts @@ -1,5 +1,6 @@ import { defineMessages } from 'react-intl'; +import { deleteEntities } from 'soapbox/entity-store/actions'; import toast from 'soapbox/toast'; import api, { getLinks } from '../api'; @@ -191,7 +192,7 @@ const updateGroupFail = (error: AxiosError) => ({ }); const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(deleteGroupRequest(id)); + dispatch(deleteEntities([id], 'Group')); return api(getState).delete(`/api/v1/groups/${id}`) .then(() => dispatch(deleteGroupSuccess(id))) diff --git a/app/soapbox/actions/importer/index.ts b/app/soapbox/actions/importer/index.ts index 8750a5d61f..8d577cd496 100644 --- a/app/soapbox/actions/importer/index.ts +++ b/app/soapbox/actions/importer/index.ts @@ -1,3 +1,7 @@ +import { importEntities } from 'soapbox/entity-store/actions'; +import { Group, groupSchema } from 'soapbox/schemas'; +import { filteredArray } from 'soapbox/schemas/utils'; + import { getSettings } from '../settings'; import type { AppDispatch, RootState } from 'soapbox/store'; @@ -18,11 +22,11 @@ const importAccount = (account: APIEntity) => const importAccounts = (accounts: APIEntity[]) => ({ type: ACCOUNTS_IMPORT, accounts }); -const importGroup = (group: APIEntity) => - ({ type: GROUP_IMPORT, group }); +const importGroup = (group: Group) => + importEntities([group], 'Group'); -const importGroups = (groups: APIEntity[]) => - ({ type: GROUPS_IMPORT, groups }); +const importGroups = (groups: Group[]) => + importEntities(groups, 'Group'); const importStatus = (status: APIEntity, idempotencyKey?: string) => (dispatch: AppDispatch, getState: () => RootState) => { @@ -69,17 +73,8 @@ const importFetchedGroup = (group: APIEntity) => importFetchedGroups([group]); const importFetchedGroups = (groups: APIEntity[]) => { - const normalGroups: APIEntity[] = []; - - const processGroup = (group: APIEntity) => { - if (!group.id) return; - - normalGroups.push(group); - }; - - groups.forEach(processGroup); - - return importGroups(normalGroups); + const entities = filteredArray(groupSchema).catch([]).parse(groups); + return importGroups(entities); }; const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) => From c51870af6e6e04c4f1b18d59939b83f5db067da5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 Mar 2023 19:26:37 -0500 Subject: [PATCH 04/34] Update some more groups stuff to use entities --- app/soapbox/features/group/group-blocked-members.tsx | 12 +++++------- .../features/group/group-membership-requests.tsx | 12 +++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/soapbox/features/group/group-blocked-members.tsx b/app/soapbox/features/group/group-blocked-members.tsx index aae717c12d..9c197da624 100644 --- a/app/soapbox/features/group/group-blocked-members.tsx +++ b/app/soapbox/features/group/group-blocked-members.tsx @@ -1,12 +1,12 @@ import React, { useCallback, useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { fetchGroup, fetchGroupBlocks, groupUnblock } from 'soapbox/actions/groups'; +import { fetchGroupBlocks, groupUnblock } from 'soapbox/actions/groups'; 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 { makeGetAccount, makeGetGroup } from 'soapbox/selectors'; +import { useAppDispatch, useAppSelector, useGroup } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; import toast from 'soapbox/toast'; import ColumnForbidden from '../ui/components/column-forbidden'; @@ -62,14 +62,12 @@ const GroupBlockedMembers: React.FC = ({ params }) => { const intl = useIntl(); const dispatch = useAppDispatch(); - const id = params?.id || ''; + const id = params?.id; - const getGroup = useCallback(makeGetGroup(), []); - const group = useAppSelector(state => getGroup(state, id)); + const { group } = useGroup(id); const accountIds = useAppSelector((state) => state.user_lists.group_blocks.get(id)?.items); useEffect(() => { - if (!group) dispatch(fetchGroup(id)); dispatch(fetchGroupBlocks(id)); }, [id]); diff --git a/app/soapbox/features/group/group-membership-requests.tsx b/app/soapbox/features/group/group-membership-requests.tsx index 13f0925aca..fd33f39475 100644 --- a/app/soapbox/features/group/group-membership-requests.tsx +++ b/app/soapbox/features/group/group-membership-requests.tsx @@ -1,12 +1,12 @@ import React, { useCallback, useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { authorizeGroupMembershipRequest, fetchGroup, fetchGroupMembershipRequests, rejectGroupMembershipRequest } from 'soapbox/actions/groups'; +import { authorizeGroupMembershipRequest, fetchGroupMembershipRequests, rejectGroupMembershipRequest } from 'soapbox/actions/groups'; 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 { makeGetAccount, makeGetGroup } from 'soapbox/selectors'; +import { useAppDispatch, useAppSelector, useGroup } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; import toast from 'soapbox/toast'; import ColumnForbidden from '../ui/components/column-forbidden'; @@ -77,14 +77,12 @@ const GroupMembershipRequests: React.FC = ({ params }) const intl = useIntl(); const dispatch = useAppDispatch(); - const id = params?.id || ''; + const id = params?.id; - const getGroup = useCallback(makeGetGroup(), []); - const group = useAppSelector(state => getGroup(state, id)); + const { group } = useGroup(id); const accountIds = useAppSelector((state) => state.user_lists.membership_requests.get(id)?.items); useEffect(() => { - if (!group) dispatch(fetchGroup(id)); dispatch(fetchGroupMembershipRequests(id)); }, [id]); From 181bf23c345cf06a6dbc501c61974fc90ac229a3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 Mar 2023 13:15:00 -0500 Subject: [PATCH 05/34] Importer: use EntityStore enums --- app/soapbox/actions/importer/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/soapbox/actions/importer/index.ts b/app/soapbox/actions/importer/index.ts index 8d577cd496..ec0ec31214 100644 --- a/app/soapbox/actions/importer/index.ts +++ b/app/soapbox/actions/importer/index.ts @@ -1,4 +1,5 @@ import { importEntities } from 'soapbox/entity-store/actions'; +import { Entities } from 'soapbox/entity-store/entities'; import { Group, groupSchema } from 'soapbox/schemas'; import { filteredArray } from 'soapbox/schemas/utils'; @@ -23,10 +24,10 @@ const importAccounts = (accounts: APIEntity[]) => ({ type: ACCOUNTS_IMPORT, accounts }); const importGroup = (group: Group) => - importEntities([group], 'Group'); + importEntities([group], Entities.GROUPS); const importGroups = (groups: Group[]) => - importEntities(groups, 'Group'); + importEntities(groups, Entities.GROUPS); const importStatus = (status: APIEntity, idempotencyKey?: string) => (dispatch: AppDispatch, getState: () => RootState) => { From e6621a802b34131ee81481955a010f0442c5bce5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 Mar 2023 13:18:36 -0500 Subject: [PATCH 06/34] Make popular and suggested groups share the Group store --- app/soapbox/entity-store/entities.ts | 2 -- app/soapbox/hooks/api/usePopularGroups.ts | 2 +- app/soapbox/hooks/api/useSuggestedGroups.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/soapbox/entity-store/entities.ts b/app/soapbox/entity-store/entities.ts index 05d7b60d9c..30220eed63 100644 --- a/app/soapbox/entity-store/entities.ts +++ b/app/soapbox/entity-store/entities.ts @@ -2,6 +2,4 @@ export enum Entities { GROUPS = 'Groups', GROUP_RELATIONSHIPS = 'GroupRelationships', GROUP_MEMBERSHIPS = 'GroupMemberships', - POPULAR_GROUPS = 'PopularGroups', - SUGGESTED_GROUPS = 'SuggestedGroups', } \ No newline at end of file diff --git a/app/soapbox/hooks/api/usePopularGroups.ts b/app/soapbox/hooks/api/usePopularGroups.ts index e856270a50..88ae48c9dc 100644 --- a/app/soapbox/hooks/api/usePopularGroups.ts +++ b/app/soapbox/hooks/api/usePopularGroups.ts @@ -9,7 +9,7 @@ function usePopularGroups() { const features = useFeatures(); const { entities, ...result } = useEntities( - [Entities.POPULAR_GROUPS, ''], + [Entities.GROUPS, 'popular'], '/api/mock/groups', // '/api/v1/truth/trends/groups' { schema: groupSchema, diff --git a/app/soapbox/hooks/api/useSuggestedGroups.ts b/app/soapbox/hooks/api/useSuggestedGroups.ts index 54477c2490..c1b85805ca 100644 --- a/app/soapbox/hooks/api/useSuggestedGroups.ts +++ b/app/soapbox/hooks/api/useSuggestedGroups.ts @@ -9,7 +9,7 @@ function useSuggestedGroups() { const features = useFeatures(); const { entities, ...result } = useEntities( - [Entities.SUGGESTED_GROUPS, ''], + [Entities.GROUPS, 'suggested'], '/api/mock/groups', // '/api/v1/truth/suggestions/groups' { schema: groupSchema, From c0a22205f713408181383e796399cd913f01ef24 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 Mar 2023 16:14:23 -0500 Subject: [PATCH 07/34] Fix GroupActionButton test --- .../group/components/__tests__/group-action-button.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/group/components/__tests__/group-action-button.test.tsx b/app/soapbox/features/group/components/__tests__/group-action-button.test.tsx index 03f80fd9cf..6809ea0093 100644 --- a/app/soapbox/features/group/components/__tests__/group-action-button.test.tsx +++ b/app/soapbox/features/group/components/__tests__/group-action-button.test.tsx @@ -98,7 +98,7 @@ describe('', () => { relationship: buildGroupRelationship({ requested: false, member: true, - role: 'admin', + role: 'owner', }), }); }); From 8b81838f2f4daa3c98ac365261f82deee676d44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 18 Mar 2023 00:07:18 +0100 Subject: [PATCH 08/34] Support custom emoji reacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/emoji-reacts.ts | 11 ++++++----- app/soapbox/components/account.tsx | 3 +++ app/soapbox/components/status-action-bar.tsx | 9 +++++---- app/soapbox/components/status-action-button.tsx | 6 ++++-- app/soapbox/components/status-reaction-wrapper.tsx | 6 +++--- .../components/ui/emoji-selector/emoji-selector.tsx | 11 ++++++----- app/soapbox/components/ui/emoji/emoji.tsx | 4 ++-- app/soapbox/features/emoji/index.ts | 2 +- .../status/components/status-interaction-bar.tsx | 1 + .../features/ui/components/modals/reactions-modal.tsx | 6 +++--- app/soapbox/reducers/statuses.ts | 2 +- app/soapbox/reducers/user-lists.ts | 1 + app/soapbox/utils/emoji-reacts.ts | 10 ++++++---- app/soapbox/utils/features.ts | 5 +++++ 14 files changed, 47 insertions(+), 30 deletions(-) diff --git a/app/soapbox/actions/emoji-reacts.ts b/app/soapbox/actions/emoji-reacts.ts index ac205d38de..746a7372f8 100644 --- a/app/soapbox/actions/emoji-reacts.ts +++ b/app/soapbox/actions/emoji-reacts.ts @@ -25,7 +25,7 @@ const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL'; const noOp = () => () => new Promise(f => f(undefined)); -const simpleEmojiReact = (status: Status, emoji: string) => +const simpleEmojiReact = (status: Status, emoji: string, custom?: string) => (dispatch: AppDispatch) => { const emojiReacts: ImmutableList> = status.pleroma.get('emoji_reactions') || ImmutableList(); @@ -43,7 +43,7 @@ const simpleEmojiReact = (status: Status, emoji: string) => if (emoji === '👍') { dispatch(favourite(status)); } else { - dispatch(emojiReact(status, emoji)); + dispatch(emojiReact(status, emoji, custom)); } }).catch(err => { console.error(err); @@ -70,11 +70,11 @@ const fetchEmojiReacts = (id: string, emoji: string) => }); }; -const emojiReact = (status: Status, emoji: string) => +const emojiReact = (status: Status, emoji: string, custom?: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return dispatch(noOp()); - dispatch(emojiReactRequest(status, emoji)); + dispatch(emojiReactRequest(status, emoji, custom)); return api(getState) .put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`) @@ -120,10 +120,11 @@ const fetchEmojiReactsFail = (id: string, error: AxiosError) => ({ error, }); -const emojiReactRequest = (status: Status, emoji: string) => ({ +const emojiReactRequest = (status: Status, emoji: string, custom?: string) => ({ type: EMOJI_REACT_REQUEST, status, emoji, + custom, skipLoading: true, }); diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index f793668956..20150b79df 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -91,6 +91,7 @@ export interface IAccount { showEdit?: boolean approvalStatus?: StatusApprovalStatus emoji?: string + emojiUrl?: string note?: string } @@ -116,6 +117,7 @@ const Account = ({ showEdit = false, approvalStatus, emoji, + emojiUrl, note, }: IAccount) => { const overflowRef = useRef(null); @@ -193,6 +195,7 @@ const Account = ({ )} diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index 8e8d9c202c..f36bb34e9c 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -538,7 +538,8 @@ const StatusActionBar: React.FC = ({ allowedEmoji, ).reduce((acc, cur) => acc + cur.get('count'), 0); - const meEmojiReact = getReactForStatus(status, allowedEmoji) as keyof typeof reactMessages | undefined; + const meEmojiReact = getReactForStatus(status, allowedEmoji); + const meEmojiName = meEmojiReact?.get('name') as keyof typeof reactMessages | undefined; const reactMessages = { '👍': messages.reactionLike, @@ -550,7 +551,7 @@ const StatusActionBar: React.FC = ({ '': messages.favourite, }; - const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite); + const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiName || ''] || messages.favourite); const menu = _makeMenu(publicStatus); let reblogIcon = require('@tabler/icons/repeat.svg'); @@ -635,7 +636,7 @@ const StatusActionBar: React.FC = ({ icon={require('@tabler/icons/heart.svg')} filled color='accent' - active={Boolean(meEmojiReact)} + active={Boolean(meEmojiName)} count={emojiReactCount} emoji={meEmojiReact} text={withLabels ? meEmojiTitle : undefined} @@ -648,7 +649,7 @@ const StatusActionBar: React.FC = ({ color='accent' filled onClick={handleFavouriteClick} - active={Boolean(meEmojiReact)} + active={Boolean(meEmojiName)} count={favouriteCount} text={withLabels ? meEmojiTitle : undefined} /> diff --git a/app/soapbox/components/status-action-button.tsx b/app/soapbox/components/status-action-button.tsx index 6fe7130f62..39795fc7ee 100644 --- a/app/soapbox/components/status-action-button.tsx +++ b/app/soapbox/components/status-action-button.tsx @@ -4,6 +4,8 @@ import React from 'react'; import { Text, Icon, Emoji } from 'soapbox/components/ui'; import { shortNumberFormat } from 'soapbox/utils/numbers'; +import type { Map as ImmutableMap } from 'immutable'; + const COLORS = { accent: 'accent', success: 'success', @@ -31,7 +33,7 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes text?: React.ReactNode } @@ -42,7 +44,7 @@ const StatusActionButton = React.forwardRef - + ); } else { diff --git a/app/soapbox/components/status-reaction-wrapper.tsx b/app/soapbox/components/status-reaction-wrapper.tsx index b95d983755..206cd1fed1 100644 --- a/app/soapbox/components/status-reaction-wrapper.tsx +++ b/app/soapbox/components/status-reaction-wrapper.tsx @@ -60,9 +60,9 @@ const StatusReactionWrapper: React.FC = ({ statusId, chi } }; - const handleReact = (emoji: string): void => { + const handleReact = (emoji: string, custom?: string): void => { if (ownAccount) { - dispatch(simpleEmojiReact(status, emoji)); + dispatch(simpleEmojiReact(status, emoji, custom)); } else { handleUnauthorized(); } @@ -71,7 +71,7 @@ const StatusReactionWrapper: React.FC = ({ statusId, chi }; const handleClick: React.EventHandler = e => { - const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji) || '👍'; + const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.get('name') || '👍'; if (isUserTouching()) { if (ownAccount) { diff --git a/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx b/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx index 961155cb3b..55e98b0487 100644 --- a/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx +++ b/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx @@ -5,9 +5,9 @@ import { usePopper } from 'react-popper'; import { Emoji as EmojiComponent, HStack, IconButton } from 'soapbox/components/ui'; import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown'; -import { useSoapboxConfig } from 'soapbox/hooks'; +import { useFeatures, useSoapboxConfig } from 'soapbox/hooks'; -import type { Emoji, NativeEmoji } from 'soapbox/features/emoji'; +import type { Emoji } from 'soapbox/features/emoji'; interface IEmojiButton { /** Unicode emoji character. */ @@ -39,7 +39,7 @@ const EmojiButton: React.FC = ({ emoji, className, onClick, tabInd interface IEmojiSelector { onClose?(): void /** Event handler when an emoji is clicked. */ - onReact(emoji: string): void + onReact(emoji: string, custom?: string): void /** Element that triggers the EmojiSelector Popper */ referenceElement: HTMLElement | null placement?: Placement @@ -62,6 +62,7 @@ const EmojiSelector: React.FC = ({ all = true, }): JSX.Element => { const soapboxConfig = useSoapboxConfig(); + const { customEmojiReacts } = useFeatures(); const [expanded, setExpanded] = useState(false); @@ -102,7 +103,7 @@ const EmojiSelector: React.FC = ({ }; const handlePickEmoji = (emoji: Emoji) => { - onReact((emoji as NativeEmoji).native); + onReact(emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined); }; useEffect(() => () => { @@ -148,7 +149,7 @@ const EmojiSelector: React.FC = ({ visible={expanded} setVisible={setExpanded} update={update} - withCustom={false} + withCustom={customEmojiReacts} onPickEmoji={handlePickEmoji} /> ) : ( diff --git a/app/soapbox/components/ui/emoji/emoji.tsx b/app/soapbox/components/ui/emoji/emoji.tsx index b631f230dc..eb8f02509d 100644 --- a/app/soapbox/components/ui/emoji/emoji.tsx +++ b/app/soapbox/components/ui/emoji/emoji.tsx @@ -10,7 +10,7 @@ interface IEmoji extends React.ImgHTMLAttributes { /** A single emoji image. */ const Emoji: React.FC = (props): JSX.Element | null => { - const { emoji, alt, ...rest } = props; + const { emoji, alt, src, ...rest } = props; const codepoints = toCodePoints(removeVS16s(emoji)); const filename = codepoints.join('-'); @@ -20,7 +20,7 @@ const Emoji: React.FC = (props): JSX.Element | null => { {alt ); diff --git a/app/soapbox/features/emoji/index.ts b/app/soapbox/features/emoji/index.ts index 8fa279dad4..61a957e9ec 100644 --- a/app/soapbox/features/emoji/index.ts +++ b/app/soapbox/features/emoji/index.ts @@ -28,7 +28,7 @@ export interface CustomEmoji { export interface NativeEmoji { id: string colons: string - custom?: boolean + custom?: false unified: string native: string } diff --git a/app/soapbox/features/status/components/status-interaction-bar.tsx b/app/soapbox/features/status/components/status-interaction-bar.tsx index fb4db14666..4d0333c062 100644 --- a/app/soapbox/features/status/components/status-interaction-bar.tsx +++ b/app/soapbox/features/status/components/status-interaction-bar.tsx @@ -154,6 +154,7 @@ const StatusInteractionBar: React.FC = ({ status }): JSX. key={i} className='h-4.5 w-4.5 flex-none' emoji={e.get('name')} + src={e.get('url')} /> ); })} diff --git a/app/soapbox/features/ui/components/modals/reactions-modal.tsx b/app/soapbox/features/ui/components/modals/reactions-modal.tsx index a31957745f..e5fbca99f7 100644 --- a/app/soapbox/features/ui/components/modals/reactions-modal.tsx +++ b/app/soapbox/features/ui/components/modals/reactions-modal.tsx @@ -54,7 +54,7 @@ const ReactionsModal: React.FC = ({ onClose, statusId, reaction reactions!.forEach(reaction => items.push( { text:
- + {reaction.count}
, action: () => setReaction(reaction.name), @@ -71,7 +71,7 @@ const ReactionsModal: React.FC = ({ onClose, statusId, reaction const accounts = reactions && (reaction ? reactions.find(({ name }) => name === reaction)?.accounts.map(account => ({ id: account, reaction: reaction })) - : reactions.map(({ accounts, name }) => accounts.map(account => ({ id: account, reaction: name }))).flatten()) as ImmutableList<{ id: string, reaction: string }>; + : reactions.map(({ accounts, name, url }) => accounts.map(account => ({ id: account, reaction: name, reactionUrl: url }))).flatten()) as ImmutableList<{ id: string, reaction: string, reactionUrl?: string }>; let body; @@ -91,7 +91,7 @@ const ReactionsModal: React.FC = ({ onClose, statusId, reaction itemClassName='pb-3' > {accounts.map((account) => - , + , )} ); diff --git a/app/soapbox/reducers/statuses.ts b/app/soapbox/reducers/statuses.ts index 66b34b7d70..11e4846a68 100644 --- a/app/soapbox/reducers/statuses.ts +++ b/app/soapbox/reducers/statuses.ts @@ -242,7 +242,7 @@ export default function statuses(state = initialState, action: AnyAction): State return state .updateIn( [action.status.get('id'), 'pleroma', 'emoji_reactions'], - emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji), + emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji, action.custom), ); case UNEMOJI_REACT_REQUEST: return state diff --git a/app/soapbox/reducers/user-lists.ts b/app/soapbox/reducers/user-lists.ts index 206cd7c810..c979e9c463 100644 --- a/app/soapbox/reducers/user-lists.ts +++ b/app/soapbox/reducers/user-lists.ts @@ -82,6 +82,7 @@ export const ReactionRecord = ImmutableRecord({ accounts: ImmutableOrderedSet(), count: 0, name: '', + url: null as string | null, }); const ReactionListRecord = ImmutableRecord({ diff --git a/app/soapbox/utils/emoji-reacts.ts b/app/soapbox/utils/emoji-reacts.ts index b9104e7837..2fa83906ba 100644 --- a/app/soapbox/utils/emoji-reacts.ts +++ b/app/soapbox/utils/emoji-reacts.ts @@ -74,19 +74,19 @@ export const reduceEmoji = (emojiReacts: ImmutableList, favouritesCo allowedEmoji, )); -export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): string | undefined => { +export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): EmojiReact | undefined => { const result = reduceEmoji( status.pleroma.get('emoji_reactions', ImmutableList()), status.favourites_count || 0, status.favourited, allowedEmoji, ).filter(e => e.get('me') === true) - .getIn([0, 'name']); + .get(0); - return typeof result === 'string' ? result : undefined; + return typeof result?.get('name') === 'string' ? result : undefined; }; -export const simulateEmojiReact = (emojiReacts: ImmutableList, emoji: string) => { +export const simulateEmojiReact = (emojiReacts: ImmutableList, emoji: string, url?: string) => { const idx = emojiReacts.findIndex(e => e.get('name') === emoji); const emojiReact = emojiReacts.get(idx); @@ -94,12 +94,14 @@ export const simulateEmojiReact = (emojiReacts: ImmutableList, emoji return emojiReacts.set(idx, emojiReact.merge({ count: emojiReact.get('count') + 1, me: true, + url, })); } else { return emojiReacts.push(ImmutableMap({ count: 1, me: true, name: emoji, + url, })); } }; diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 6fe794b571..e99d4c9219 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -324,6 +324,11 @@ const getInstanceFeatures = (instance: Instance) => { v.software === TAKAHE, ]), + /** + * Ability to add non-standard reactions to a status. + */ + customEmojiReacts: v.software === PLEROMA && gte(v.version, '2.5.50'), + /** * Legacy DMs timeline where messages are displayed chronologically without groupings. * @see GET /api/v1/timelines/direct From 179eb7fc993d2d7322c1069339e7658445aebda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 18 Mar 2023 11:55:00 +0100 Subject: [PATCH 09/34] Fix preview in reactions modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../notifications/components/notification.tsx | 1 + .../ui/components/modals/reactions-modal.tsx | 26 ++++++++++++++----- app/soapbox/normalizers/notification.ts | 1 + 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index 5df21c6478..6812cdeb88 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -269,6 +269,7 @@ const Notification: React.FC = (props) => { return ( ); diff --git a/app/soapbox/features/ui/components/modals/reactions-modal.tsx b/app/soapbox/features/ui/components/modals/reactions-modal.tsx index e5fbca99f7..08e4afa43a 100644 --- a/app/soapbox/features/ui/components/modals/reactions-modal.tsx +++ b/app/soapbox/features/ui/components/modals/reactions-modal.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import { List as ImmutableList } from 'immutable'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions'; @@ -17,6 +17,12 @@ const messages = defineMessages({ all: { id: 'reactions.all', defaultMessage: 'All' }, }); +interface IAccountWithReaction { + id: string + reaction: string + reactionUrl?: string +} + interface IReactionsModal { onClose: (string: string) => void statusId: string @@ -65,17 +71,25 @@ const ReactionsModal: React.FC = ({ onClose, statusId, reaction return ; }; + const accounts = useMemo((): ImmutableList | undefined => { + if (!reactions) return; + + if (reaction) { + const reactionRecord = reactions.find(({ name }) => name === reaction); + + if (reactionRecord) return reactionRecord.accounts.map(account => ({ id: account, reaction: reaction, reactionUrl: reactionRecord.url || undefined })).toList(); + } else { + return reactions.map(({ accounts, name, url }) => accounts.map(account => ({ id: account, reaction: name, reactionUrl: url }))).flatten() as ImmutableList; + } + }, [reactions, reaction]); + useEffect(() => { fetchData(); }, []); - const accounts = reactions && (reaction - ? reactions.find(({ name }) => name === reaction)?.accounts.map(account => ({ id: account, reaction: reaction })) - : reactions.map(({ accounts, name, url }) => accounts.map(account => ({ id: account, reaction: name, reactionUrl: url }))).flatten()) as ImmutableList<{ id: string, reaction: string, reactionUrl?: string }>; - let body; - if (!accounts) { + if (!accounts || !reactions) { body = ; } else { const emptyMessage = ; diff --git a/app/soapbox/normalizers/notification.ts b/app/soapbox/normalizers/notification.ts index 9b34278b6e..2412db05de 100644 --- a/app/soapbox/normalizers/notification.ts +++ b/app/soapbox/normalizers/notification.ts @@ -17,6 +17,7 @@ export const NotificationRecord = ImmutableRecord({ chat_message: null as ImmutableMap | string | null, // pleroma:chat_mention created_at: new Date(), emoji: null as string | null, // pleroma:emoji_reaction + emoji_url: null as string | null, // pleroma:emoji_reaction id: '', status: null as EmbeddedEntity, target: null as EmbeddedEntity, // move From 09a0a3693597976293610f9088ba2c29f83aae42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 18 Mar 2023 13:27:27 +0100 Subject: [PATCH 10/34] Update tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../utils/__tests__/emoji-reacts.test.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/app/soapbox/utils/__tests__/emoji-reacts.test.ts b/app/soapbox/utils/__tests__/emoji-reacts.test.ts index f5c6f150c1..9b901614cd 100644 --- a/app/soapbox/utils/__tests__/emoji-reacts.test.ts +++ b/app/soapbox/utils/__tests__/emoji-reacts.test.ts @@ -52,15 +52,15 @@ describe('mergeEmojiFavourites', () => { describe('with existing 👍 reacts', () => { const emojiReacts = fromJS([ - { 'count': 20, 'me': false, 'name': '👍' }, - { 'count': 15, 'me': false, 'name': '❤' }, - { 'count': 7, 'me': false, 'name': '😯' }, + { 'count': 20, 'me': false, 'name': '👍', 'url': undefined }, + { 'count': 15, 'me': false, 'name': '❤', 'url': undefined }, + { 'count': 7, 'me': false, 'name': '😯', 'url': undefined }, ]) as ImmutableList>; it('combines 👍 reacts with favourites', () => { expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([ - { 'count': 32, 'me': true, 'name': '👍' }, - { 'count': 15, 'me': false, 'name': '❤' }, - { 'count': 7, 'me': false, 'name': '😯' }, + { 'count': 32, 'me': true, 'name': '👍', 'url': undefined }, + { 'count': 15, 'me': false, 'name': '❤', 'url': undefined }, + { 'count': 7, 'me': false, 'name': '😯', 'url': undefined }, ])); }); }); @@ -146,12 +146,12 @@ describe('getReactForStatus', () => { ], }, })); - expect(getReactForStatus(status, ALLOWED_EMOJI)).toEqual('❤'); + expect(getReactForStatus(status, ALLOWED_EMOJI)?.get('name')).toEqual('❤'); }); it('returns a thumbs-up for a favourite', () => { const status = normalizeStatus(fromJS({ favourites_count: 1, favourited: true })); - expect(getReactForStatus(status)).toEqual('👍'); + expect(getReactForStatus(status)?.get('name')).toEqual('👍'); }); it('returns undefined when a status has no reacts (or favourites)', () => { @@ -173,24 +173,24 @@ describe('getReactForStatus', () => { describe('simulateEmojiReact', () => { it('adds the emoji to the list', () => { const emojiReacts = fromJS([ - { 'count': 2, 'me': false, 'name': '👍' }, - { 'count': 2, 'me': false, 'name': '❤' }, + { 'count': 2, 'me': false, 'name': '👍', 'url': undefined }, + { 'count': 2, 'me': false, 'name': '❤', 'url': undefined }, ]) as ImmutableList>; expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([ - { 'count': 2, 'me': false, 'name': '👍' }, - { 'count': 3, 'me': true, 'name': '❤' }, + { 'count': 2, 'me': false, 'name': '👍', 'url': undefined }, + { 'count': 3, 'me': true, 'name': '❤', 'url': undefined }, ])); }); it('creates the emoji if it didn\'t already exist', () => { const emojiReacts = fromJS([ - { 'count': 2, 'me': false, 'name': '👍' }, - { 'count': 2, 'me': false, 'name': '❤' }, + { 'count': 2, 'me': false, 'name': '👍', 'url': undefined }, + { 'count': 2, 'me': false, 'name': '❤', 'url': undefined }, ]) as ImmutableList>; expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([ - { 'count': 2, 'me': false, 'name': '👍' }, - { 'count': 2, 'me': false, 'name': '❤' }, - { 'count': 1, 'me': true, 'name': '😯' }, + { 'count': 2, 'me': false, 'name': '👍', 'url': undefined }, + { 'count': 2, 'me': false, 'name': '❤', 'url': undefined }, + { 'count': 1, 'me': true, 'name': '😯', 'url': undefined }, ])); }); }); From 38c99dbc48c4a6ea760a47125297a59d0692bc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 18 Mar 2023 20:49:13 +0100 Subject: [PATCH 11/34] Add tests from custom reacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../utils/__tests__/emoji-reacts.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/soapbox/utils/__tests__/emoji-reacts.test.ts b/app/soapbox/utils/__tests__/emoji-reacts.test.ts index 9b901614cd..5d64fdf3dc 100644 --- a/app/soapbox/utils/__tests__/emoji-reacts.test.ts +++ b/app/soapbox/utils/__tests__/emoji-reacts.test.ts @@ -193,6 +193,18 @@ describe('simulateEmojiReact', () => { { 'count': 1, 'me': true, 'name': '😯', 'url': undefined }, ])); }); + + it('adds a custom emoji to the list', () => { + const emojiReacts = fromJS([ + { 'count': 2, 'me': false, 'name': '👍', 'url': undefined }, + { 'count': 2, 'me': false, 'name': '❤', 'url': undefined }, + ]) as ImmutableList>; + expect(simulateEmojiReact(emojiReacts, 'soapbox', 'https://gleasonator.com/emoji/Gleasonator/soapbox.png')).toEqual(fromJS([ + { 'count': 2, 'me': false, 'name': '👍', 'url': undefined }, + { 'count': 2, 'me': false, 'name': '❤', 'url': undefined }, + { 'count': 1, 'me': true, 'name': 'soapbox', 'url': 'https://gleasonator.com/emoji/Gleasonator/soapbox.png' }, + ])); + }); }); describe('simulateUnEmojiReact', () => { @@ -218,4 +230,16 @@ describe('simulateUnEmojiReact', () => { { 'count': 2, 'me': false, 'name': '❤' }, ])); }); + + it ('removes custom emoji from the list', () => { + const emojiReacts = fromJS([ + { 'count': 2, 'me': false, 'name': '👍' }, + { 'count': 2, 'me': false, 'name': '❤' }, + { 'count': 1, 'me': true, 'name': 'soapbox', 'url': 'https://gleasonator.com/emoji/Gleasonator/soapbox.png' }, + ]) as ImmutableList>; + expect(simulateUnEmojiReact(emojiReacts, 'soapbox')).toEqual(fromJS([ + { 'count': 2, 'me': false, 'name': '👍' }, + { 'count': 2, 'me': false, 'name': '❤' }, + ])); + }); }); From 049554db8468337ffdcbdcd04abc0e49dadc00be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 19 Mar 2023 20:23:36 +0100 Subject: [PATCH 12/34] Add missing gap to PendingStatus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../features/ui/components/pending-status.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/soapbox/features/ui/components/pending-status.tsx b/app/soapbox/features/ui/components/pending-status.tsx index ba8e9e8d74..d8ee1ab33c 100644 --- a/app/soapbox/features/ui/components/pending-status.tsx +++ b/app/soapbox/features/ui/components/pending-status.tsx @@ -4,7 +4,7 @@ import React from 'react'; import Account from 'soapbox/components/account'; import StatusContent from 'soapbox/components/status-content'; import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; -import { Card, HStack } from 'soapbox/components/ui'; +import { Card, HStack, Stack } from 'soapbox/components/ui'; import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card'; import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder-media-gallery'; import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container'; @@ -78,16 +78,18 @@ const PendingStatus: React.FC = ({ idempotencyKey, className, mu
- + + - + - {status.poll && } + {status.poll && } - {status.quote && } + {status.quote && } +
{/* TODO */} From 8634c5f91e11c2977937ec91e600b1f9a8ae7929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 19 Mar 2023 19:26:17 +0000 Subject: [PATCH 13/34] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index afca6bb714..b927650dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Posts: Support posts filtering on recent Mastodon versions +- Reactions: Support custom emoji reactions ### Changed - Posts: truncate Nostr pubkeys in reply mentions. From 813fd7f8eebacc25c3bb94b5074189f4550cc8b5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 Mar 2023 15:39:58 -0500 Subject: [PATCH 14/34] Delete proprietary groups image --- app/assets/images/group.svg | 26 ------------------- .../manage-group-modal/steps/privacy-step.tsx | 5 ---- 2 files changed, 31 deletions(-) delete mode 100644 app/assets/images/group.svg diff --git a/app/assets/images/group.svg b/app/assets/images/group.svg deleted file mode 100644 index 4e187999db..0000000000 --- a/app/assets/images/group.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx index e26243f9aa..22ad893053 100644 --- a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx +++ b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx @@ -18,11 +18,6 @@ const PrivacyStep = () => { return ( <> - From f7c7b6ce6ca8d76e6c435d460bfa8fb1730d3cbd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 Mar 2023 15:47:40 -0500 Subject: [PATCH 15/34] Update CHANGELOG --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b927650dea..0e04be8028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Posts: Support posts filtering on recent Mastodon versions - Reactions: Support custom emoji reactions +- Compatbility: Support Mastodon v2 timeline filters. ### Changed - Posts: truncate Nostr pubkeys in reply mentions. +- Posts: upgraded emoji picker component. ### Fixed - Posts: fixed emojis being cut off in reactions modal. - Posts: fix audio player progress bar visibility. +- Posts: added missing gap in pending status. +- Compatibility: fixed quote posting compatibility with custom Pleroma forks. +- Profile: fix "load more" button height on account gallery page. +- 18n: fixed Chinese language being detected from the browser. +- Conversations: fixed pagination (Mastodon). ## [3.2.0] - 2023-02-15 From 2b75dcacd23044dfec860bd979d57af5935907c4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 Mar 2023 17:24:39 -0500 Subject: [PATCH 16/34] EmojiPickerDropdownContainer: switch to floating-ui --- .../components/emoji-picker-dropdown.tsx | 3 +- .../emoji-picker-dropdown-container.tsx | 63 +++++++------------ 2 files changed, 24 insertions(+), 42 deletions(-) diff --git a/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx b/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx index 7e6bd24140..3f486502e8 100644 --- a/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx +++ b/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx @@ -11,7 +11,6 @@ import { RootState } from 'soapbox/store'; import { buildCustomEmojis } from '../../emoji'; import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; -import type { State as PopperState } from '@popperjs/core'; import type { Emoji, CustomEmoji, NativeEmoji } from 'soapbox/features/emoji'; let EmojiPicker: any; // load asynchronously @@ -49,7 +48,7 @@ export interface IEmojiPickerDropdown { withCustom?: boolean visible: boolean setVisible: (value: boolean) => void - update: (() => Promise>) | null + update: (() => any) | null } const perLine = 8; diff --git a/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx b/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx index 47dea8d6f3..5929d1711e 100644 --- a/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx +++ b/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx @@ -1,17 +1,14 @@ +import { useFloating, shift } from '@floating-ui/react'; import clsx from 'clsx'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import React, { KeyboardEvent, useEffect, useState } from 'react'; +import React, { KeyboardEvent, useState } from 'react'; import { createPortal } from 'react-dom'; import { defineMessages, useIntl } from 'react-intl'; -import { usePopper } from 'react-popper'; import { IconButton } from 'soapbox/components/ui'; -import { isMobile } from 'soapbox/is-mobile'; +import { useClickOutside } from 'soapbox/hooks'; import EmojiPickerDropdown, { IEmojiPickerDropdown } from '../components/emoji-picker-dropdown'; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - export const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, }); @@ -22,51 +19,28 @@ const EmojiPickerDropdownContainer = ( const intl = useIntl(); const title = intl.formatMessage(messages.emoji); - const [popperElement, setPopperElement] = useState(null); - const [popperReference, setPopperReference] = useState(null); - const [containerElement, setContainerElement] = useState(null); - const [visible, setVisible] = useState(false); - const placement = props.condensed ? 'bottom-start' : 'top-start'; - const { styles, attributes, update } = usePopper(popperReference, popperElement, { - placement: isMobile(window.innerWidth) ? 'auto' : placement, + const { x, y, strategy, refs, update } = useFloating({ + middleware: [shift()], }); - const handleDocClick = (e: any) => { - if (!containerElement?.contains(e.target) && !popperElement?.contains(e.target)) { - setVisible(false); - } - }; + useClickOutside(refs, () => { + setVisible(false); + }); const handleToggle = (e: MouseEvent | KeyboardEvent) => { e.stopPropagation(); setVisible(!visible); }; - // TODO: move to class - const style: React.CSSProperties = !isMobile(window.innerWidth) ? styles.popper : { - ...styles.popper, width: '100%', - }; - - useEffect(() => { - document.addEventListener('click', handleDocClick, false); - document.addEventListener('touchend', handleDocClick, listenerOptions); - - return () => { - document.removeEventListener('click', handleDocClick, false); - // @ts-ignore - document.removeEventListener('touchend', handleDocClick, listenerOptions); - }; - }); - return ( -
+
- +
, document.body, )} From 5c7c0ea1ddf415d568200325a01dea0eeca32d47 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 Mar 2023 17:52:45 -0500 Subject: [PATCH 17/34] EmojiSelector: switch to floating-ui --- .../ui/emoji-selector/emoji-selector.tsx | 76 +++++-------------- .../chat-message-reaction-wrapper.tsx | 1 - 2 files changed, 19 insertions(+), 58 deletions(-) diff --git a/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx b/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx index 55e98b0487..7053fbb7ce 100644 --- a/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx +++ b/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx @@ -1,11 +1,10 @@ -import { Placement } from '@popperjs/core'; +import { shift, useFloating, Placement } from '@floating-ui/react'; import clsx from 'clsx'; import React, { useEffect, useState } from 'react'; -import { usePopper } from 'react-popper'; import { Emoji as EmojiComponent, HStack, IconButton } from 'soapbox/components/ui'; import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown'; -import { useFeatures, useSoapboxConfig } from 'soapbox/hooks'; +import { useClickOutside, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; import type { Emoji } from 'soapbox/features/emoji'; @@ -45,8 +44,6 @@ interface IEmojiSelector { placement?: Placement /** Whether the selector should be visible. */ visible?: boolean - /** X/Y offset of the floating picker. */ - offset?: [number, number] /** Whether to allow any emoji to be chosen. */ all?: boolean } @@ -58,7 +55,6 @@ const EmojiSelector: React.FC = ({ onReact, placement = 'top', visible = false, - offset = [-10, 0], all = true, }): JSX.Element => { const soapboxConfig = useSoapboxConfig(); @@ -66,36 +62,9 @@ const EmojiSelector: React.FC = ({ const [expanded, setExpanded] = useState(false); - // `useRef` won't trigger a re-render, while `useState` does. - // https://popper.js.org/react-popper/v2/ - const [popperElement, setPopperElement] = useState(null); - - const handleClickOutside = (event: MouseEvent) => { - if ([referenceElement, popperElement, document.querySelector('em-emoji-picker')].some(el => el?.contains(event.target as Node))) { - return; - } - - if (document.querySelector('em-emoji-picker')) { - event.preventDefault(); - event.stopPropagation(); - return setExpanded(false); - } - - if (onClose) { - onClose(); - } - }; - - const { styles, attributes, update } = usePopper(referenceElement, popperElement, { + const { x, y, strategy, refs, update } = useFloating({ placement, - modifiers: [ - { - name: 'offset', - options: { - offset, - }, - }, - ], + middleware: [shift()], }); const handleExpand: React.MouseEventHandler = () => { @@ -106,6 +75,10 @@ const EmojiSelector: React.FC = ({ onReact(emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined); }; + useEffect(() => { + refs.setReference(referenceElement); + }, [referenceElement]); + useEffect(() => () => { document.body.style.overflow = ''; }, []); @@ -114,35 +87,24 @@ const EmojiSelector: React.FC = ({ setExpanded(false); }, [visible]); - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [referenceElement, popperElement]); - - useEffect(() => { - if (visible && update) { - update(); + useClickOutside(refs, () => { + if (onClose) { + onClose(); } - }, [visible, update]); - - useEffect(() => { - if (expanded && update) { - update(); - } - }, [expanded, update]); - + }); return (
{expanded ? ( setIsOpen(false)} - offset={[-10, 12]} all={false} /> From 47561e5c018a45cd2c1717505402d7332a9a7a19 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 Mar 2023 17:53:14 -0500 Subject: [PATCH 18/34] Enable custom emoji reacts on custom Pleroma forks --- app/soapbox/utils/features.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index e99d4c9219..b435406aa3 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -327,7 +327,11 @@ const getInstanceFeatures = (instance: Instance) => { /** * Ability to add non-standard reactions to a status. */ - customEmojiReacts: v.software === PLEROMA && gte(v.version, '2.5.50'), + customEmojiReacts: any([ + features.includes('pleroma_custom_emoji_reactions'), + features.includes('custom_emoji_reactions'), + v.software === PLEROMA && gte(v.version, '2.5.50'), + ]), /** * Legacy DMs timeline where messages are displayed chronologically without groupings. From 67ffe9609f6950f246bc6e955fa311b6a85b552a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 Mar 2023 18:03:44 -0500 Subject: [PATCH 19/34] ChatTextarea: pass ref to child Fixes https://gitlab.com/soapbox-pub/soapbox/-/issues/1390 --- app/soapbox/features/chats/components/chat-textarea.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/chats/components/chat-textarea.tsx b/app/soapbox/features/chats/components/chat-textarea.tsx index 111a4cdc92..f6ee67b931 100644 --- a/app/soapbox/features/chats/components/chat-textarea.tsx +++ b/app/soapbox/features/chats/components/chat-textarea.tsx @@ -14,13 +14,13 @@ interface IChatTextarea extends React.ComponentProps { } /** Custom textarea for chats. */ -const ChatTextarea: React.FC = ({ +const ChatTextarea: React.FC = React.forwardRef(({ attachments, onDeleteAttachment, uploadCount = 0, uploadProgress = 0, ...rest -}) => { +}, ref) => { const isUploading = uploadCount > 0; const handleDeleteAttachment = (i: number) => { @@ -64,9 +64,9 @@ const ChatTextarea: React.FC = ({ )} -