-type AsyncComponent = () => Promise<{ default: React.ComponentType
}>
-
-const withHoc =
(asyncComponent: AsyncComponent
, hoc: HOC
) => {
- return async () => {
- const { default: component } = await asyncComponent();
- return { default: hoc(component) };
- };
-};
-
-export default withHoc;
\ No newline at end of file
diff --git a/app/soapbox/components/icon-button.tsx b/app/soapbox/components/icon-button.tsx
deleted file mode 100644
index f4da2c2c5..000000000
--- a/app/soapbox/components/icon-button.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import clsx from 'clsx';
-import React from 'react';
-
-import Icon from 'soapbox/components/icon';
-
-interface IIconButton extends Pick, 'className' | 'disabled' | 'onClick' | 'onKeyDown' | 'onKeyPress' | 'onKeyUp' | 'onMouseDown' | 'onMouseEnter' | 'onMouseLeave' | 'tabIndex' | 'title'> {
- active?: boolean
- expanded?: boolean
- iconClassName?: string
- pressed?: boolean
- size?: number
- src: string
- text?: React.ReactNode
-}
-
-const IconButton: React.FC = ({
- active,
- className,
- disabled,
- expanded,
- iconClassName,
- onClick,
- onKeyDown,
- onKeyUp,
- onKeyPress,
- onMouseDown,
- onMouseEnter,
- onMouseLeave,
- pressed,
- size = 18,
- src,
- tabIndex = 0,
- text,
- title,
-}) => {
-
- const handleClick: React.MouseEventHandler = (e) => {
- e.preventDefault();
-
- if (!disabled && onClick) {
- onClick(e);
- }
- };
-
- const handleMouseDown: React.MouseEventHandler = (e) => {
- if (!disabled && onMouseDown) {
- onMouseDown(e);
- }
- };
-
- const handleKeyDown: React.KeyboardEventHandler = (e) => {
- if (!disabled && onKeyDown) {
- onKeyDown(e);
- }
- };
-
- const handleKeyUp: React.KeyboardEventHandler = (e) => {
- if (!disabled && onKeyUp) {
- onKeyUp(e);
- }
- };
-
- const handleKeyPress: React.KeyboardEventHandler = (e) => {
- if (onKeyPress && !disabled) {
- onKeyPress(e);
- }
- };
-
- const classes = clsx(className, 'icon-button', {
- active,
- disabled,
- });
-
- return (
-
- );
-};
-
-export default IconButton;
diff --git a/app/soapbox/components/pending-items-row.tsx b/app/soapbox/components/pending-items-row.tsx
deleted file mode 100644
index 4fbf236cd..000000000
--- a/app/soapbox/components/pending-items-row.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import clsx from 'clsx';
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router-dom';
-
-import { HStack, Icon, Text } from 'soapbox/components/ui';
-
-interface IPendingItemsRow {
- /** Path to navigate the user when clicked. */
- to: string
- /** Number of pending items. */
- count: number
- /** Size of the icon. */
- size?: 'md' | 'lg'
-}
-
-const PendingItemsRow: React.FC = ({ to, count, size = 'md' }) => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export { PendingItemsRow };
\ No newline at end of file
diff --git a/app/soapbox/components/status-reaction-wrapper.tsx b/app/soapbox/components/status-reaction-wrapper.tsx
deleted file mode 100644
index 206cd1fed..000000000
--- a/app/soapbox/components/status-reaction-wrapper.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-
-import { simpleEmojiReact } from 'soapbox/actions/emoji-reacts';
-import { openModal } from 'soapbox/actions/modals';
-import { EmojiSelector, Portal } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
-import { isUserTouching } from 'soapbox/is-mobile';
-import { getReactForStatus } from 'soapbox/utils/emoji-reacts';
-
-interface IStatusReactionWrapper {
- statusId: string
- children: JSX.Element
-}
-
-/** Provides emoji reaction functionality to the underlying button component */
-const StatusReactionWrapper: React.FC = ({ statusId, children }): JSX.Element | null => {
- const dispatch = useAppDispatch();
- const ownAccount = useOwnAccount();
- const status = useAppSelector(state => state.statuses.get(statusId));
- const soapboxConfig = useSoapboxConfig();
-
- const timeout = useRef();
- const [visible, setVisible] = useState(false);
-
- const [referenceElement, setReferenceElement] = useState(null);
-
- useEffect(() => {
- return () => {
- if (timeout.current) {
- clearTimeout(timeout.current);
- }
- };
- }, []);
-
- if (!status) return null;
-
- const handleMouseEnter = () => {
- if (timeout.current) {
- clearTimeout(timeout.current);
- }
-
- if (!isUserTouching()) {
- setVisible(true);
- }
- };
-
- const handleMouseLeave = () => {
- if (timeout.current) {
- clearTimeout(timeout.current);
- }
-
- // Unless the user is touching, delay closing the emoji selector briefly
- // so the user can move the mouse diagonally to make a selection.
- if (isUserTouching()) {
- setVisible(false);
- } else {
- timeout.current = setTimeout(() => {
- setVisible(false);
- }, 500);
- }
- };
-
- const handleReact = (emoji: string, custom?: string): void => {
- if (ownAccount) {
- dispatch(simpleEmojiReact(status, emoji, custom));
- } else {
- handleUnauthorized();
- }
-
- setVisible(false);
- };
-
- const handleClick: React.EventHandler = e => {
- const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.get('name') || '👍';
-
- if (isUserTouching()) {
- if (ownAccount) {
- if (visible) {
- handleReact(meEmojiReact);
- } else {
- setVisible(true);
- }
- } else {
- handleUnauthorized();
- }
- } else {
- handleReact(meEmojiReact);
- }
-
- e.preventDefault();
- e.stopPropagation();
- };
-
- const handleUnauthorized = () => {
- dispatch(openModal('UNAUTHORIZED', {
- action: 'FAVOURITE',
- ap_id: status.url,
- }));
- };
-
- return (
-
- {React.cloneElement(children, {
- onClick: handleClick,
- ref: setReferenceElement,
- })}
-
- {visible && (
-
- setVisible(false)}
- />
-
- )}
-
- );
-};
-
-export default StatusReactionWrapper;
diff --git a/app/soapbox/components/statuses/status-info.tsx b/app/soapbox/components/statuses/status-info.tsx
deleted file mode 100644
index 50fad86b3..000000000
--- a/app/soapbox/components/statuses/status-info.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-
-import { HStack, Text } from '../ui';
-
-interface IStatusInfo {
- avatarSize: number
- icon: React.ReactNode
- text: React.ReactNode
-}
-
-const StatusInfo = (props: IStatusInfo) => {
- const { avatarSize, icon, text } = props;
-
- const onClick = (event: React.MouseEvent) => {
- event.stopPropagation();
- };
-
- return (
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions
-
-
-
- {icon}
-
-
-
- {text}
-
-
-
- );
-};
-
-export default StatusInfo;
\ No newline at end of file
diff --git a/app/soapbox/components/ui/carousel/carousel.tsx b/app/soapbox/components/ui/carousel/carousel.tsx
deleted file mode 100644
index 441c1b1d1..000000000
--- a/app/soapbox/components/ui/carousel/carousel.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import React, { useEffect, useState } from 'react';
-
-import { useDimensions } from 'soapbox/hooks';
-
-import HStack from '../hstack/hstack';
-import Icon from '../icon/icon';
-
-interface ICarousel {
- children: any
- /** Optional height to force on controls */
- controlsHeight?: number
- /** How many items in the carousel */
- itemCount: number
- /** The minimum width per item */
- itemWidth: number
- /** Should the controls be disabled? */
- isDisabled?: boolean
-}
-
-/**
- * Carousel
- */
-const Carousel: React.FC = (props): JSX.Element => {
- const { children, controlsHeight, isDisabled, itemCount, itemWidth } = props;
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [ref, setContainerRef, { width: finalContainerWidth }] = useDimensions();
- const containerWidth = finalContainerWidth || ref?.clientWidth;
-
- const [pageSize, setPageSize] = useState(0);
- const [currentPage, setCurrentPage] = useState(1);
-
- const numberOfPages = Math.ceil(itemCount / pageSize);
- const width = containerWidth / (Math.floor(containerWidth / itemWidth));
-
- const hasNextPage = currentPage < numberOfPages && numberOfPages > 1;
- const hasPrevPage = currentPage > 1 && numberOfPages > 1;
-
- const handleNextPage = () => setCurrentPage((prevPage) => prevPage + 1);
- const handlePrevPage = () => setCurrentPage((prevPage) => prevPage - 1);
-
- const renderChildren = () => {
- if (typeof children === 'function') {
- return children({ width: width || 'auto' });
- }
-
- return children;
- };
-
- useEffect(() => {
- if (containerWidth) {
- setPageSize(Math.round(containerWidth / width));
- }
- }, [containerWidth, width]);
-
- return (
-
-
-
-
-
-
-
- {renderChildren()}
-
-
-
-
-
-
-
- );
-};
-
-export default Carousel;
\ No newline at end of file
diff --git a/app/soapbox/components/ui/popover/popover.tsx b/app/soapbox/components/ui/popover/popover.tsx
deleted file mode 100644
index 7f909f8bc..000000000
--- a/app/soapbox/components/ui/popover/popover.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import {
- arrow,
- autoPlacement,
- FloatingArrow,
- offset,
- useClick,
- useDismiss,
- useFloating,
- useHover,
- useInteractions,
- useTransitionStyles,
-} from '@floating-ui/react';
-import clsx from 'clsx';
-import React, { useRef, useState } from 'react';
-
-import Portal from '../portal/portal';
-
-interface IPopover {
- children: React.ReactElement>
- /** The content of the popover */
- content: React.ReactNode
- /** Should we remove padding on the Popover */
- isFlush?: boolean
- /** Should the popover trigger via click or hover */
- interaction?: 'click' | 'hover'
- /** Add a class to the reference (trigger) element */
- referenceElementClassName?: string
-}
-
-/**
- * Popover
- *
- * Similar to tooltip, but requires a click and is used for larger blocks
- * of information.
- */
-const Popover: React.FC = (props) => {
- const { children, content, referenceElementClassName, interaction = 'hover', isFlush = false } = props;
-
- const [isOpen, setIsOpen] = useState(false);
-
- const arrowRef = useRef(null);
-
- const { x, y, strategy, refs, context } = useFloating({
- open: isOpen,
- onOpenChange: setIsOpen,
- placement: 'top',
- middleware: [
- autoPlacement({
- allowedPlacements: ['top', 'bottom'],
- }),
- offset(10),
- arrow({
- element: arrowRef,
- }),
- ],
- });
-
- const { isMounted, styles } = useTransitionStyles(context, {
- initial: {
- opacity: 0,
- transform: 'scale(0.8)',
- },
- duration: {
- open: 200,
- close: 200,
- },
- });
-
- const click = useClick(context, { enabled: interaction === 'click' });
- const hover = useHover(context, { enabled: interaction === 'hover' });
- const dismiss = useDismiss(context);
-
- const { getReferenceProps, getFloatingProps } = useInteractions([
- click,
- hover,
- dismiss,
- ]);
-
- return (
- <>
- {React.cloneElement(children, {
- ref: refs.setReference,
- ...getReferenceProps(),
- className: clsx(children.props.className, referenceElementClassName),
- })}
-
- {(isMounted) && (
-
-
- {content}
-
-
-
-
- )}
- >
- );
-};
-
-export default Popover;
\ No newline at end of file
diff --git a/app/soapbox/components/ui/portal/portal.tsx b/app/soapbox/components/ui/portal/portal.tsx
deleted file mode 100644
index 4f83b98b4..000000000
--- a/app/soapbox/components/ui/portal/portal.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React, { useLayoutEffect, useState } from 'react';
-import ReactDOM from 'react-dom';
-
-interface IPortal {
- children: React.ReactNode
-}
-
-/**
- * Portal
- */
-const Portal: React.FC = ({ children }) => {
- const [isRendered, setIsRendered] = useState(false);
-
- useLayoutEffect(() => {
- setIsRendered(true);
- }, []);
-
-
- if (!isRendered) {
- return null;
- }
-
- return (
- ReactDOM.createPortal(
- children,
- document.getElementById('soapbox') as HTMLDivElement,
- )
- );
-};
-
-export default Portal;
\ No newline at end of file
diff --git a/app/soapbox/containers/account-container.tsx b/app/soapbox/containers/account-container.tsx
deleted file mode 100644
index 54c6db64e..000000000
--- a/app/soapbox/containers/account-container.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { useCallback } from 'react';
-
-import { useAppSelector } from 'soapbox/hooks';
-
-import Account, { IAccount } from '../components/account';
-import { makeGetAccount } from '../selectors';
-
-interface IAccountContainer extends Omit {
- id: string
-}
-
-const AccountContainer: React.FC = ({ id, ...props }) => {
- const getAccount = useCallback(makeGetAccount(), []);
- const account = useAppSelector(state => getAccount(state, id));
-
- return (
-
- );
-};
-
-export default AccountContainer;
diff --git a/app/soapbox/entity-store/__tests__/reducer.test.ts b/app/soapbox/entity-store/__tests__/reducer.test.ts
deleted file mode 100644
index 3e6aa3510..000000000
--- a/app/soapbox/entity-store/__tests__/reducer.test.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-import {
- deleteEntities,
- dismissEntities,
- entitiesFetchFail,
- entitiesFetchRequest,
- entitiesFetchSuccess,
- importEntities,
- incrementEntities,
-} from '../actions';
-import reducer, { State } from '../reducer';
-import { createListState } from '../utils';
-
-import type { EntityCache } from '../types';
-
-interface TestEntity {
- id: string
- msg: string
-}
-
-test('import entities', () => {
- const entities: TestEntity[] = [
- { id: '1', msg: 'yolo' },
- { id: '2', msg: 'benis' },
- { id: '3', msg: 'boop' },
- ];
-
- const action = importEntities(entities, 'TestEntity');
- const result = reducer(undefined, action);
- const cache = result.TestEntity as EntityCache;
-
- expect(cache.store['1']!.msg).toBe('yolo');
- expect(Object.values(cache.lists).length).toBe(0);
-});
-
-test('import entities into a list', () => {
- const entities: TestEntity[] = [
- { id: '1', msg: 'yolo' },
- { id: '2', msg: 'benis' },
- { id: '3', msg: 'boop' },
- ];
-
- const action = importEntities(entities, 'TestEntity', 'thingies');
- const result = reducer(undefined, action);
- const cache = result.TestEntity as EntityCache;
-
- expect(cache.store['2']!.msg).toBe('benis');
- expect(cache.lists.thingies!.ids.size).toBe(3);
- expect(cache.lists.thingies!.state.totalCount).toBe(3);
-
- // Now try adding an additional item.
- const entities2: TestEntity[] = [
- { id: '4', msg: 'hehe' },
- ];
-
- const action2 = importEntities(entities2, 'TestEntity', 'thingies');
- const result2 = reducer(result, action2);
- const cache2 = result2.TestEntity as EntityCache;
-
- expect(cache2.store['4']!.msg).toBe('hehe');
- expect(cache2.lists.thingies!.ids.size).toBe(4);
- expect(cache2.lists.thingies!.state.totalCount).toBe(4);
-
- // Finally, update an item.
- const entities3: TestEntity[] = [
- { id: '2', msg: 'yolofam' },
- ];
-
- const action3 = importEntities(entities3, 'TestEntity', 'thingies');
- const result3 = reducer(result2, action3);
- const cache3 = result3.TestEntity as EntityCache;
-
- expect(cache3.store['2']!.msg).toBe('yolofam');
- expect(cache3.lists.thingies!.ids.size).toBe(4); // unchanged
- expect(cache3.lists.thingies!.state.totalCount).toBe(4);
-});
-
-test('fetching updates the list state', () => {
- const action = entitiesFetchRequest('TestEntity', 'thingies');
- const result = reducer(undefined, action);
-
- expect(result.TestEntity!.lists.thingies!.state.fetching).toBe(true);
-});
-
-test('failure adds the error to the state', () => {
- const error = new Error('whoopsie');
-
- const action = entitiesFetchFail('TestEntity', 'thingies', error);
- const result = reducer(undefined, action);
-
- expect(result.TestEntity!.lists.thingies!.state.error).toBe(error);
-});
-
-test('import entities with override', () => {
- const state: State = {
- TestEntity: {
- store: { '1': { id: '1' }, '2': { id: '2' }, '3': { id: '3' } },
- lists: {
- thingies: {
- ids: new Set(['1', '2', '3']),
- state: { ...createListState(), totalCount: 3 },
- },
- },
- },
- };
-
- const entities: TestEntity[] = [
- { id: '4', msg: 'yolo' },
- { id: '5', msg: 'benis' },
- ];
-
- const now = new Date();
-
- const action = entitiesFetchSuccess(entities, 'TestEntity', 'thingies', 'end', {
- next: undefined,
- prev: undefined,
- totalCount: 2,
- error: null,
- fetched: true,
- fetching: false,
- lastFetchedAt: now,
- invalid: false,
- }, true);
-
- const result = reducer(state, action);
- const cache = result.TestEntity as EntityCache;
-
- expect([...cache.lists.thingies!.ids]).toEqual(['4', '5']);
- expect(cache.lists.thingies!.state.lastFetchedAt).toBe(now); // Also check that newState worked
-});
-
-test('deleting items', () => {
- const state: State = {
- TestEntity: {
- store: { '1': { id: '1' }, '2': { id: '2' }, '3': { id: '3' } },
- lists: {
- '': {
- ids: new Set(['1', '2', '3']),
- state: { ...createListState(), totalCount: 3 },
- },
- },
- },
- };
-
- const action = deleteEntities(['3', '1'], 'TestEntity');
- const result = reducer(state, action);
-
- expect(result.TestEntity!.store).toMatchObject({ '2': { id: '2' } });
- expect([...result.TestEntity!.lists['']!.ids]).toEqual(['2']);
- expect(result.TestEntity!.lists['']!.state.totalCount).toBe(1);
-});
-
-test('dismiss items', () => {
- const state: State = {
- TestEntity: {
- store: { '1': { id: '1' }, '2': { id: '2' }, '3': { id: '3' } },
- lists: {
- yolo: {
- ids: new Set(['1', '2', '3']),
- state: { ...createListState(), totalCount: 3 },
- },
- },
- },
- };
-
- const action = dismissEntities(['3', '1'], 'TestEntity', 'yolo');
- const result = reducer(state, action);
-
- expect(result.TestEntity!.store).toMatchObject(state.TestEntity!.store);
- expect([...result.TestEntity!.lists.yolo!.ids]).toEqual(['2']);
- expect(result.TestEntity!.lists.yolo!.state.totalCount).toBe(1);
-});
-
-test('increment items', () => {
- const state: State = {
- TestEntity: {
- store: { '1': { id: '1' }, '2': { id: '2' }, '3': { id: '3' } },
- lists: {
- thingies: {
- ids: new Set(['1', '2', '3']),
- state: { ...createListState(), totalCount: 3 },
- },
- },
- },
- };
-
- const action = incrementEntities('TestEntity', 'thingies', 1);
- const result = reducer(state, action);
-
- expect(result.TestEntity!.lists.thingies!.state.totalCount).toBe(4);
-});
-
-test('decrement items', () => {
- const state: State = {
- TestEntity: {
- store: { '1': { id: '1' }, '2': { id: '2' }, '3': { id: '3' } },
- lists: {
- thingies: {
- ids: new Set(['1', '2', '3']),
- state: { ...createListState(), totalCount: 3 },
- },
- },
- },
- };
-
- const action = incrementEntities('TestEntity', 'thingies', -1);
- const result = reducer(state, action);
-
- expect(result.TestEntity!.lists.thingies!.state.totalCount).toBe(2);
-});
\ No newline at end of file
diff --git a/app/soapbox/entity-store/actions.ts b/app/soapbox/entity-store/actions.ts
deleted file mode 100644
index bb96255c6..000000000
--- a/app/soapbox/entity-store/actions.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import type { Entity, EntityListState, ImportPosition } from './types';
-
-const ENTITIES_IMPORT = 'ENTITIES_IMPORT' as const;
-const ENTITIES_DELETE = 'ENTITIES_DELETE' as const;
-const ENTITIES_DISMISS = 'ENTITIES_DISMISS' as const;
-const ENTITIES_INCREMENT = 'ENTITIES_INCREMENT' as const;
-const ENTITIES_FETCH_REQUEST = 'ENTITIES_FETCH_REQUEST' as const;
-const ENTITIES_FETCH_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const;
-const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const;
-const ENTITIES_INVALIDATE_LIST = 'ENTITIES_INVALIDATE_LIST' as const;
-
-/** Action to import entities into the cache. */
-function importEntities(entities: Entity[], entityType: string, listKey?: string, pos?: ImportPosition) {
- return {
- type: ENTITIES_IMPORT,
- entityType,
- entities,
- listKey,
- pos,
- };
-}
-
-interface DeleteEntitiesOpts {
- preserveLists?: boolean
-}
-
-function deleteEntities(ids: Iterable, entityType: string, opts: DeleteEntitiesOpts = {}) {
- return {
- type: ENTITIES_DELETE,
- ids,
- entityType,
- opts,
- };
-}
-
-function dismissEntities(ids: Iterable, entityType: string, listKey: string) {
- return {
- type: ENTITIES_DISMISS,
- ids,
- entityType,
- listKey,
- };
-}
-
-function incrementEntities(entityType: string, listKey: string, diff: number) {
- return {
- type: ENTITIES_INCREMENT,
- entityType,
- listKey,
- diff,
- };
-}
-
-function entitiesFetchRequest(entityType: string, listKey?: string) {
- return {
- type: ENTITIES_FETCH_REQUEST,
- entityType,
- listKey,
- };
-}
-
-function entitiesFetchSuccess(
- entities: Entity[],
- entityType: string,
- listKey?: string,
- pos?: ImportPosition,
- newState?: EntityListState,
- overwrite = false,
-) {
- return {
- type: ENTITIES_FETCH_SUCCESS,
- entityType,
- entities,
- listKey,
- pos,
- newState,
- overwrite,
- };
-}
-
-function entitiesFetchFail(entityType: string, listKey: string | undefined, error: any) {
- return {
- type: ENTITIES_FETCH_FAIL,
- entityType,
- listKey,
- error,
- };
-}
-
-function invalidateEntityList(entityType: string, listKey: string) {
- return {
- type: ENTITIES_INVALIDATE_LIST,
- entityType,
- listKey,
- };
-}
-
-/** Any action pertaining to entities. */
-type EntityAction =
- ReturnType
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType;
-
-export {
- ENTITIES_IMPORT,
- ENTITIES_DELETE,
- ENTITIES_DISMISS,
- ENTITIES_INCREMENT,
- ENTITIES_FETCH_REQUEST,
- ENTITIES_FETCH_SUCCESS,
- ENTITIES_FETCH_FAIL,
- ENTITIES_INVALIDATE_LIST,
- importEntities,
- deleteEntities,
- dismissEntities,
- incrementEntities,
- entitiesFetchRequest,
- entitiesFetchSuccess,
- entitiesFetchFail,
- invalidateEntityList,
- EntityAction,
-};
-
-export type { DeleteEntitiesOpts };
\ No newline at end of file
diff --git a/app/soapbox/entity-store/entities.ts b/app/soapbox/entity-store/entities.ts
deleted file mode 100644
index 9878cbbf2..000000000
--- a/app/soapbox/entity-store/entities.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export enum Entities {
- ACCOUNTS = 'Accounts',
- GROUPS = 'Groups',
- GROUP_MEMBERSHIPS = 'GroupMemberships',
- GROUP_RELATIONSHIPS = 'GroupRelationships',
- GROUP_TAGS = 'GroupTags',
- RELATIONSHIPS = 'Relationships',
- STATUSES = 'Statuses'
-}
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/index.ts b/app/soapbox/entity-store/hooks/index.ts
deleted file mode 100644
index b95d2d1af..000000000
--- a/app/soapbox/entity-store/hooks/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export { useEntities } from './useEntities';
-export { useEntity } from './useEntity';
-export { useEntityActions } from './useEntityActions';
-export { useEntityLookup } from './useEntityLookup';
-export { useCreateEntity } from './useCreateEntity';
-export { useDeleteEntity } from './useDeleteEntity';
-export { useDismissEntity } from './useDismissEntity';
-export { useIncrementEntity } from './useIncrementEntity';
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/types.ts b/app/soapbox/entity-store/hooks/types.ts
deleted file mode 100644
index 95ba8b016..000000000
--- a/app/soapbox/entity-store/hooks/types.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { Entity } from '../types';
-import type { AxiosResponse } from 'axios';
-import type z from 'zod';
-
-type EntitySchema = z.ZodType;
-
-/**
- * Tells us where to find/store the entity in the cache.
- * This value is accepted in hooks, but needs to be parsed into an `EntitiesPath`
- * before being passed to the store.
- */
-type ExpandedEntitiesPath = [
- /** Name of the entity type for use in the global cache, eg `'Notification'`. */
- entityType: string,
- /**
- * Name of a particular index of this entity type.
- * Multiple params get combined into one string with a `:` separator.
- */
- ...listKeys: string[],
-]
-
-/** Used to look up an entity in a list. */
-type EntitiesPath = [entityType: string, listKey: string]
-
-/** Used to look up a single entity by its ID. */
-type EntityPath = [entityType: string, entityId: string]
-
-/** Callback functions for entity actions. */
-interface EntityCallbacks {
- onSuccess?(value: Value): void
- onError?(error: Error): void
-}
-
-/**
- * Passed into hooks to make requests.
- * Must return an Axios response.
- */
-type EntityFn = (value: T) => Promise
-
-export type {
- EntitySchema,
- ExpandedEntitiesPath,
- EntitiesPath,
- EntityPath,
- EntityCallbacks,
- EntityFn,
-};
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/useCreateEntity.ts b/app/soapbox/entity-store/hooks/useCreateEntity.ts
deleted file mode 100644
index 24ce3af7d..000000000
--- a/app/soapbox/entity-store/hooks/useCreateEntity.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { AxiosError } from 'axios';
-import { z } from 'zod';
-
-import { useAppDispatch, useLoading } from 'soapbox/hooks';
-
-import { importEntities } from '../actions';
-
-import { parseEntitiesPath } from './utils';
-
-import type { Entity } from '../types';
-import type { EntityCallbacks, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
-
-interface UseCreateEntityOpts {
- schema?: EntitySchema
-}
-
-function useCreateEntity(
- expandedPath: ExpandedEntitiesPath,
- entityFn: EntityFn,
- opts: UseCreateEntityOpts = {},
-) {
- const dispatch = useAppDispatch();
-
- const [isSubmitting, setPromise] = useLoading();
- const { entityType, listKey } = parseEntitiesPath(expandedPath);
-
- async function createEntity(data: Data, callbacks: EntityCallbacks = {}): Promise {
- try {
- const result = await setPromise(entityFn(data));
- const schema = opts.schema || z.custom();
- const entity = schema.parse(result.data);
-
- // TODO: optimistic updating
- dispatch(importEntities([entity], entityType, listKey, 'start'));
-
- if (callbacks.onSuccess) {
- callbacks.onSuccess(entity);
- }
- } catch (error) {
- if (error instanceof AxiosError) {
- if (callbacks.onError) {
- callbacks.onError(error);
- }
- } else {
- throw error;
- }
- }
- }
-
- return {
- createEntity,
- isSubmitting,
- };
-}
-
-export { useCreateEntity };
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/useDeleteEntity.ts b/app/soapbox/entity-store/hooks/useDeleteEntity.ts
deleted file mode 100644
index dac1d9a26..000000000
--- a/app/soapbox/entity-store/hooks/useDeleteEntity.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { useAppDispatch, useGetState, useLoading } from 'soapbox/hooks';
-
-import { deleteEntities, importEntities } from '../actions';
-
-import type { EntityCallbacks, EntityFn } from './types';
-
-/**
- * Optimistically deletes an entity from the store.
- * This hook should be used to globally delete an entity from all lists.
- * To remove an entity from a single list, see `useDismissEntity`.
- */
-function useDeleteEntity(
- entityType: string,
- entityFn: EntityFn,
-) {
- const dispatch = useAppDispatch();
- const getState = useGetState();
- const [isSubmitting, setPromise] = useLoading();
-
- async function deleteEntity(entityId: string, callbacks: EntityCallbacks = {}): Promise {
- // Get the entity before deleting, so we can reverse the action if the API request fails.
- const entity = getState().entities[entityType]?.store[entityId];
-
- // Optimistically delete the entity from the _store_ but keep the lists in tact.
- dispatch(deleteEntities([entityId], entityType, { preserveLists: true }));
-
- try {
- await setPromise(entityFn(entityId));
-
- // Success - finish deleting entity from the state.
- dispatch(deleteEntities([entityId], entityType));
-
- if (callbacks.onSuccess) {
- callbacks.onSuccess(entityId);
- }
- } catch (e) {
- if (entity) {
- // If the API failed, reimport the entity.
- dispatch(importEntities([entity], entityType));
- }
-
- if (callbacks.onError) {
- callbacks.onError(e);
- }
- }
- }
-
- return {
- deleteEntity,
- isSubmitting,
- };
-}
-
-export { useDeleteEntity };
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/useDismissEntity.ts b/app/soapbox/entity-store/hooks/useDismissEntity.ts
deleted file mode 100644
index b09e35951..000000000
--- a/app/soapbox/entity-store/hooks/useDismissEntity.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useAppDispatch, useLoading } from 'soapbox/hooks';
-
-import { dismissEntities } from '../actions';
-
-import { parseEntitiesPath } from './utils';
-
-import type { EntityFn, ExpandedEntitiesPath } from './types';
-
-/**
- * Removes an entity from a specific list.
- * To remove an entity globally from all lists, see `useDeleteEntity`.
- */
-function useDismissEntity(expandedPath: ExpandedEntitiesPath, entityFn: EntityFn) {
- const dispatch = useAppDispatch();
-
- const [isLoading, setPromise] = useLoading();
- const { entityType, listKey } = parseEntitiesPath(expandedPath);
-
- // TODO: optimistic dismissing
- async function dismissEntity(entityId: string) {
- const result = await setPromise(entityFn(entityId));
- dispatch(dismissEntities([entityId], entityType, listKey));
- return result;
- }
-
- return {
- dismissEntity,
- isLoading,
- };
-}
-
-export { useDismissEntity };
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/useEntities.ts b/app/soapbox/entity-store/hooks/useEntities.ts
deleted file mode 100644
index cd413f487..000000000
--- a/app/soapbox/entity-store/hooks/useEntities.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-import { useEffect } from 'react';
-import z from 'zod';
-
-import { getNextLink, getPrevLink } from 'soapbox/api';
-import { useApi, useAppDispatch, useAppSelector, useGetState } from 'soapbox/hooks';
-import { filteredArray } from 'soapbox/schemas/utils';
-import { realNumberSchema } from 'soapbox/utils/numbers';
-
-import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess, invalidateEntityList } from '../actions';
-
-import { parseEntitiesPath } from './utils';
-
-import type { Entity, EntityListState } from '../types';
-import type { EntitiesPath, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
-import type { RootState } from 'soapbox/store';
-
-/** Additional options for the hook. */
-interface UseEntitiesOpts {
- /** A zod schema to parse the API entities. */
- schema?: EntitySchema
- /**
- * Time (milliseconds) until this query becomes stale and should be refetched.
- * It is 1 minute by default, and can be set to `Infinity` to opt-out of automatic fetching.
- */
- staleTime?: number
- /** A flag to potentially disable sending requests to the API. */
- enabled?: boolean
-}
-
-/** A hook for fetching and displaying API entities. */
-function useEntities(
- /** Tells us where to find/store the entity in the cache. */
- expandedPath: ExpandedEntitiesPath,
- /** API route to GET, eg `'/api/v1/notifications'`. If undefined, nothing will be fetched. */
- entityFn: EntityFn,
- /** Additional options for the hook. */
- opts: UseEntitiesOpts = {},
-) {
- const api = useApi();
- const dispatch = useAppDispatch();
- const getState = useGetState();
-
- const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
- const entities = useAppSelector(state => selectEntities(state, path));
-
- const isEnabled = opts.enabled ?? true;
- const isFetching = useListState(path, 'fetching');
- const lastFetchedAt = useListState(path, 'lastFetchedAt');
- const isFetched = useListState(path, 'fetched');
- const isError = !!useListState(path, 'error');
- const totalCount = useListState(path, 'totalCount');
- const isInvalid = useListState(path, 'invalid');
-
- const next = useListState(path, 'next');
- const prev = useListState(path, 'prev');
-
- const fetchPage = async(req: EntityFn, pos: 'start' | 'end', overwrite = false): Promise => {
- // Get `isFetching` state from the store again to prevent race conditions.
- const isFetching = selectListState(getState(), path, 'fetching');
- if (isFetching) return;
-
- dispatch(entitiesFetchRequest(entityType, listKey));
- try {
- const response = await req();
- const schema = opts.schema || z.custom();
- const entities = filteredArray(schema).parse(response.data);
- const parsedCount = realNumberSchema.safeParse(response.headers['x-total-count']);
- const totalCount = parsedCount.success ? parsedCount.data : undefined;
-
- dispatch(entitiesFetchSuccess(entities, entityType, listKey, pos, {
- next: getNextLink(response),
- prev: getPrevLink(response),
- totalCount: Number(totalCount) >= entities.length ? totalCount : undefined,
- fetching: false,
- fetched: true,
- error: null,
- lastFetchedAt: new Date(),
- invalid: false,
- }, overwrite));
- } catch (error) {
- dispatch(entitiesFetchFail(entityType, listKey, error));
- }
- };
-
- const fetchEntities = async(): Promise => {
- await fetchPage(entityFn, 'end', true);
- };
-
- const fetchNextPage = async(): Promise => {
- if (next) {
- await fetchPage(() => api.get(next), 'end');
- }
- };
-
- const fetchPreviousPage = async(): Promise => {
- if (prev) {
- await fetchPage(() => api.get(prev), 'start');
- }
- };
-
- const invalidate = () => {
- dispatch(invalidateEntityList(entityType, listKey));
- };
-
- const staleTime = opts.staleTime ?? 60000;
-
- useEffect(() => {
- if (!isEnabled) return;
- if (isFetching) return;
- const isUnset = !lastFetchedAt;
- const isStale = lastFetchedAt ? Date.now() >= lastFetchedAt.getTime() + staleTime : false;
-
- if (isInvalid || isUnset || isStale) {
- fetchEntities();
- }
- }, [isEnabled, ...path]);
-
- return {
- entities,
- fetchEntities,
- fetchNextPage,
- fetchPreviousPage,
- hasNextPage: !!next,
- hasPreviousPage: !!prev,
- totalCount,
- isError,
- isFetched,
- isFetching,
- isLoading: isFetching && entities.length === 0,
- invalidate,
- /** The `X-Total-Count` from the API if available, or the length of items in the store. */
- count: typeof totalCount === 'number' ? totalCount : entities.length,
- };
-}
-
-/** Get cache at path from Redux. */
-const selectCache = (state: RootState, path: EntitiesPath) => state.entities[path[0]];
-
-/** Get list at path from Redux. */
-const selectList = (state: RootState, path: EntitiesPath) => {
- const [, ...listKeys] = path;
- const listKey = listKeys.join(':');
-
- return selectCache(state, path)?.lists[listKey];
-};
-
-/** Select a particular item from a list state. */
-function selectListState(state: RootState, path: EntitiesPath, key: K) {
- const listState = selectList(state, path)?.state;
- return listState ? listState[key] : undefined;
-}
-
-/** Hook to get a particular item from a list state. */
-function useListState(path: EntitiesPath, key: K) {
- return useAppSelector(state => selectListState(state, path, key));
-}
-
-/** Get list of entities from Redux. */
-function selectEntities(state: RootState, path: EntitiesPath): readonly TEntity[] {
- const cache = selectCache(state, path);
- const list = selectList(state, path);
-
- const entityIds = list?.ids;
-
- return entityIds ? (
- Array.from(entityIds).reduce((result, id) => {
- const entity = cache?.store[id];
- if (entity) {
- result.push(entity as TEntity);
- }
- return result;
- }, [])
- ) : [];
-}
-
-export {
- useEntities,
-};
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/useEntity.ts b/app/soapbox/entity-store/hooks/useEntity.ts
deleted file mode 100644
index 3d57c8ab0..000000000
--- a/app/soapbox/entity-store/hooks/useEntity.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { useEffect } from 'react';
-import z from 'zod';
-
-import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks';
-
-import { importEntities } from '../actions';
-
-import type { Entity } from '../types';
-import type { EntitySchema, EntityPath, EntityFn } from './types';
-
-/** Additional options for the hook. */
-interface UseEntityOpts {
- /** A zod schema to parse the API entity. */
- schema?: EntitySchema
- /** Whether to refetch this entity every time the hook mounts, even if it's already in the store. */
- refetch?: boolean
- /** A flag to potentially disable sending requests to the API. */
- enabled?: boolean
-}
-
-function useEntity(
- path: EntityPath,
- entityFn: EntityFn,
- opts: UseEntityOpts = {},
-) {
- const [isFetching, setPromise] = useLoading(true);
- const dispatch = useAppDispatch();
-
- const [entityType, entityId] = path;
-
- const defaultSchema = z.custom();
- const schema = opts.schema || defaultSchema;
-
- const entity = useAppSelector(state => state.entities[entityType]?.store[entityId] as TEntity | undefined);
-
- const isEnabled = opts.enabled ?? true;
- const isLoading = isFetching && !entity;
-
- const fetchEntity = async () => {
- try {
- const response = await setPromise(entityFn());
- const entity = schema.parse(response.data);
- dispatch(importEntities([entity], entityType));
- } catch (e) {
- // do nothing
- }
- };
-
- useEffect(() => {
- if (!isEnabled) return;
- if (!entity || opts.refetch) {
- fetchEntity();
- }
- }, [isEnabled]);
-
- return {
- entity,
- fetchEntity,
- isFetching,
- isLoading,
- };
-}
-
-export {
- useEntity,
- type UseEntityOpts,
-};
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/useEntityActions.ts b/app/soapbox/entity-store/hooks/useEntityActions.ts
deleted file mode 100644
index c7e2e431d..000000000
--- a/app/soapbox/entity-store/hooks/useEntityActions.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useApi } from 'soapbox/hooks';
-
-import { useCreateEntity } from './useCreateEntity';
-import { useDeleteEntity } from './useDeleteEntity';
-import { parseEntitiesPath } from './utils';
-
-import type { Entity } from '../types';
-import type { EntitySchema, ExpandedEntitiesPath } from './types';
-
-interface UseEntityActionsOpts {
- schema?: EntitySchema
-}
-
-interface EntityActionEndpoints {
- delete?: string
- patch?: string
- post?: string
-}
-
-function useEntityActions(
- expandedPath: ExpandedEntitiesPath,
- endpoints: EntityActionEndpoints,
- opts: UseEntityActionsOpts = {},
-) {
- const api = useApi();
- const { entityType, path } = parseEntitiesPath(expandedPath);
-
- const { deleteEntity, isSubmitting: deleteSubmitting } =
- useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replaceAll(':id', entityId)));
-
- const { createEntity, isSubmitting: createSubmitting } =
- useCreateEntity(path, (data) => api.post(endpoints.post!, data), opts);
-
- const { createEntity: updateEntity, isSubmitting: updateSubmitting } =
- useCreateEntity(path, (data) => api.patch(endpoints.patch!, data), opts);
-
- return {
- createEntity,
- deleteEntity,
- updateEntity,
- isSubmitting: createSubmitting || deleteSubmitting || updateSubmitting,
- };
-}
-
-export { useEntityActions };
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/useEntityLookup.ts b/app/soapbox/entity-store/hooks/useEntityLookup.ts
deleted file mode 100644
index 73b2ef938..000000000
--- a/app/soapbox/entity-store/hooks/useEntityLookup.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { useEffect } from 'react';
-import { z } from 'zod';
-
-import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks';
-import { type RootState } from 'soapbox/store';
-
-import { importEntities } from '../actions';
-import { Entity } from '../types';
-
-import { EntityFn } from './types';
-import { type UseEntityOpts } from './useEntity';
-
-/** Entities will be filtered through this function until it returns true. */
-type LookupFn = (entity: TEntity) => boolean
-
-function useEntityLookup(
- entityType: string,
- lookupFn: LookupFn,
- entityFn: EntityFn,
- opts: UseEntityOpts = {},
-) {
- const { schema = z.custom() } = opts;
-
- const dispatch = useAppDispatch();
- const [isFetching, setPromise] = useLoading(true);
-
- const entity = useAppSelector(state => findEntity(state, entityType, lookupFn));
- const isEnabled = opts.enabled ?? true;
- const isLoading = isFetching && !entity;
-
- const fetchEntity = async () => {
- try {
- const response = await setPromise(entityFn());
- const entity = schema.parse(response.data);
- dispatch(importEntities([entity], entityType));
- } catch (e) {
- // do nothing
- }
- };
-
- useEffect(() => {
- if (!isEnabled) return;
-
- if (!entity || opts.refetch) {
- fetchEntity();
- }
- }, [isEnabled]);
-
- return {
- entity,
- fetchEntity,
- isFetching,
- isLoading,
- };
-}
-
-function findEntity(
- state: RootState,
- entityType: string,
- lookupFn: LookupFn,
-) {
- const cache = state.entities[entityType];
-
- if (cache) {
- return (Object.values(cache.store) as TEntity[]).find(lookupFn);
- }
-}
-
-export { useEntityLookup };
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/useIncrementEntity.ts b/app/soapbox/entity-store/hooks/useIncrementEntity.ts
deleted file mode 100644
index 2b09cc445..000000000
--- a/app/soapbox/entity-store/hooks/useIncrementEntity.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useAppDispatch, useLoading } from 'soapbox/hooks';
-
-import { incrementEntities } from '../actions';
-
-import { parseEntitiesPath } from './utils';
-
-import type { EntityFn, ExpandedEntitiesPath } from './types';
-
-/**
- * Increases (or decreases) the `totalCount` in the entity list by the specified amount.
- * This only works if the API returns an `X-Total-Count` header and your components read it.
- */
-function useIncrementEntity(
- expandedPath: ExpandedEntitiesPath,
- diff: number,
- entityFn: EntityFn,
-) {
- const dispatch = useAppDispatch();
- const [isLoading, setPromise] = useLoading();
- const { entityType, listKey } = parseEntitiesPath(expandedPath);
-
- async function incrementEntity(entityId: string): Promise {
- dispatch(incrementEntities(entityType, listKey, diff));
- try {
- await setPromise(entityFn(entityId));
- } catch (e) {
- dispatch(incrementEntities(entityType, listKey, diff * -1));
- }
- }
-
- return {
- incrementEntity,
- isLoading,
- };
-}
-
-export { useIncrementEntity };
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/utils.ts b/app/soapbox/entity-store/hooks/utils.ts
deleted file mode 100644
index 8b9269a2e..000000000
--- a/app/soapbox/entity-store/hooks/utils.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { EntitiesPath, ExpandedEntitiesPath } from './types';
-
-function parseEntitiesPath(expandedPath: ExpandedEntitiesPath) {
- const [entityType, ...listKeys] = expandedPath;
- const listKey = (listKeys || []).join(':');
- const path: EntitiesPath = [entityType, listKey];
-
- return {
- entityType,
- listKey,
- path,
- };
-}
-
-
-export { parseEntitiesPath };
\ No newline at end of file
diff --git a/app/soapbox/entity-store/reducer.ts b/app/soapbox/entity-store/reducer.ts
deleted file mode 100644
index ef7b604d9..000000000
--- a/app/soapbox/entity-store/reducer.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-import produce, { enableMapSet } from 'immer';
-
-import {
- ENTITIES_IMPORT,
- ENTITIES_DELETE,
- ENTITIES_DISMISS,
- ENTITIES_FETCH_REQUEST,
- ENTITIES_FETCH_SUCCESS,
- ENTITIES_FETCH_FAIL,
- EntityAction,
- ENTITIES_INVALIDATE_LIST,
- ENTITIES_INCREMENT,
-} from './actions';
-import { createCache, createList, updateStore, updateList } from './utils';
-
-import type { DeleteEntitiesOpts } from './actions';
-import type { Entity, EntityCache, EntityListState, ImportPosition } from './types';
-
-enableMapSet();
-
-/** Entity reducer state. */
-interface State {
- [entityType: string]: EntityCache | undefined
-}
-
-/** Import entities into the cache. */
-const importEntities = (
- state: State,
- entityType: string,
- entities: Entity[],
- listKey?: string,
- pos?: ImportPosition,
- newState?: EntityListState,
- overwrite = false,
-): State => {
- return produce(state, draft => {
- const cache = draft[entityType] ?? createCache();
- cache.store = updateStore(cache.store, entities);
-
- if (typeof listKey === 'string') {
- let list = cache.lists[listKey] ?? createList();
-
- if (overwrite) {
- list.ids = new Set();
- }
-
- list = updateList(list, entities, pos);
-
- if (newState) {
- list.state = newState;
- }
-
- cache.lists[listKey] = list;
- }
-
- draft[entityType] = cache;
- });
-};
-
-const deleteEntities = (
- state: State,
- entityType: string,
- ids: Iterable,
- opts: DeleteEntitiesOpts,
-) => {
- return produce(state, draft => {
- const cache = draft[entityType] ?? createCache();
-
- for (const id of ids) {
- delete cache.store[id];
-
- if (!opts?.preserveLists) {
- for (const list of Object.values(cache.lists)) {
- if (list) {
- list.ids.delete(id);
-
- if (typeof list.state.totalCount === 'number') {
- list.state.totalCount--;
- }
- }
- }
- }
- }
-
- draft[entityType] = cache;
- });
-};
-
-const dismissEntities = (
- state: State,
- entityType: string,
- ids: Iterable,
- listKey: string,
-) => {
- return produce(state, draft => {
- const cache = draft[entityType] ?? createCache();
- const list = cache.lists[listKey];
-
- if (list) {
- for (const id of ids) {
- list.ids.delete(id);
-
- if (typeof list.state.totalCount === 'number') {
- list.state.totalCount--;
- }
- }
-
- draft[entityType] = cache;
- }
- });
-};
-
-const incrementEntities = (
- state: State,
- entityType: string,
- listKey: string,
- diff: number,
-) => {
- return produce(state, draft => {
- const cache = draft[entityType] ?? createCache();
- const list = cache.lists[listKey];
-
- if (typeof list?.state?.totalCount === 'number') {
- list.state.totalCount += diff;
- draft[entityType] = cache;
- }
- });
-};
-
-const setFetching = (
- state: State,
- entityType: string,
- listKey: string | undefined,
- isFetching: boolean,
- error?: any,
-) => {
- return produce(state, draft => {
- const cache = draft[entityType] ?? createCache();
-
- if (typeof listKey === 'string') {
- const list = cache.lists[listKey] ?? createList();
- list.state.fetching = isFetching;
- list.state.error = error;
- cache.lists[listKey] = list;
- }
-
- draft[entityType] = cache;
- });
-};
-
-const invalidateEntityList = (state: State, entityType: string, listKey: string) => {
- return produce(state, draft => {
- const cache = draft[entityType] ?? createCache();
- const list = cache.lists[listKey] ?? createList();
- list.state.invalid = true;
- });
-};
-
-/** Stores various entity data and lists in a one reducer. */
-function reducer(state: Readonly = {}, action: EntityAction): State {
- switch (action.type) {
- case ENTITIES_IMPORT:
- return importEntities(state, action.entityType, action.entities, action.listKey, action.pos);
- case ENTITIES_DELETE:
- return deleteEntities(state, action.entityType, action.ids, action.opts);
- case ENTITIES_DISMISS:
- return dismissEntities(state, action.entityType, action.ids, action.listKey);
- case ENTITIES_INCREMENT:
- return incrementEntities(state, action.entityType, action.listKey, action.diff);
- case ENTITIES_FETCH_SUCCESS:
- return importEntities(state, action.entityType, action.entities, action.listKey, action.pos, action.newState, action.overwrite);
- case ENTITIES_FETCH_REQUEST:
- return setFetching(state, action.entityType, action.listKey, true);
- case ENTITIES_FETCH_FAIL:
- return setFetching(state, action.entityType, action.listKey, false, action.error);
- case ENTITIES_INVALIDATE_LIST:
- return invalidateEntityList(state, action.entityType, action.listKey);
- default:
- return state;
- }
-}
-
-export default reducer;
-export type { State };
\ No newline at end of file
diff --git a/app/soapbox/entity-store/types.ts b/app/soapbox/entity-store/types.ts
deleted file mode 100644
index 5fff2f474..000000000
--- a/app/soapbox/entity-store/types.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/** A Mastodon API entity. */
-interface Entity {
- /** Unique ID for the entity (usually the primary key in the database). */
- id: string
-}
-
-/** Store of entities by ID. */
-interface EntityStore {
- [id: string]: TEntity | undefined
-}
-
-/** List of entity IDs and fetch state. */
-interface EntityList {
- /** Set of entity IDs in this list. */
- ids: Set
- /** Server state for this entity list. */
- state: EntityListState
-}
-
-/** Fetch state for an entity list. */
-interface EntityListState {
- /** Next URL for pagination, if any. */
- next: string | undefined
- /** Previous URL for pagination, if any. */
- prev: string | undefined
- /** Total number of items according to the API. */
- totalCount: number | undefined
- /** Error returned from the API, if any. */
- error: any
- /** Whether data has already been fetched */
- fetched: boolean
- /** Whether data for this list is currently being fetched. */
- fetching: boolean
- /** Date of the last API fetch for this list. */
- lastFetchedAt: Date | undefined
- /** Whether the entities should be refetched on the next component mount. */
- invalid: boolean
-}
-
-/** Cache data pertaining to a paritcular entity type.. */
-interface EntityCache {
- /** Map of entities of this type. */
- store: EntityStore
- /** Lists of entity IDs for a particular purpose. */
- lists: {
- [listKey: string]: EntityList | undefined
- }
-}
-
-/** Whether to import items at the start or end of the list. */
-type ImportPosition = 'start' | 'end'
-
-export {
- Entity,
- EntityStore,
- EntityList,
- EntityListState,
- EntityCache,
- ImportPosition,
-};
\ No newline at end of file
diff --git a/app/soapbox/entity-store/utils.ts b/app/soapbox/entity-store/utils.ts
deleted file mode 100644
index 58d54465a..000000000
--- a/app/soapbox/entity-store/utils.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import type { Entity, EntityStore, EntityList, EntityCache, EntityListState, ImportPosition } from './types';
-
-/** Insert the entities into the store. */
-const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => {
- return entities.reduce((store, entity) => {
- store[entity.id] = entity;
- return store;
- }, { ...store });
-};
-
-/** Update the list with new entity IDs. */
-const updateList = (list: EntityList, entities: Entity[], pos: ImportPosition = 'end'): EntityList => {
- const newIds = entities.map(entity => entity.id);
- const oldIds = Array.from(list.ids);
- const ids = new Set(pos === 'start' ? [...newIds, ...oldIds] : [...oldIds, ...newIds]);
-
- if (typeof list.state.totalCount === 'number') {
- const sizeDiff = ids.size - list.ids.size;
- list.state.totalCount += sizeDiff;
- }
-
- return {
- ...list,
- ids,
- };
-};
-
-/** Create an empty entity cache. */
-const createCache = (): EntityCache => ({
- store: {},
- lists: {},
-});
-
-/** Create an empty entity list. */
-const createList = (): EntityList => ({
- ids: new Set(),
- state: createListState(),
-});
-
-/** Create an empty entity list state. */
-const createListState = (): EntityListState => ({
- next: undefined,
- prev: undefined,
- totalCount: 0,
- error: null,
- fetched: false,
- fetching: false,
- lastFetchedAt: undefined,
- invalid: false,
-});
-
-export {
- updateStore,
- updateList,
- createCache,
- createList,
- createListState,
-};
\ No newline at end of file
diff --git a/app/soapbox/features/admin/announcements.tsx b/app/soapbox/features/admin/announcements.tsx
deleted file mode 100644
index c4ab66571..000000000
--- a/app/soapbox/features/admin/announcements.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import React, { useEffect } from 'react';
-import { FormattedDate, FormattedMessage, defineMessages, useIntl } from 'react-intl';
-
-import { deleteAnnouncement, fetchAdminAnnouncements, initAnnouncementModal } from 'soapbox/actions/admin';
-import { openModal } from 'soapbox/actions/modals';
-import ScrollableList from 'soapbox/components/scrollable-list';
-import { Button, Column, HStack, Stack, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-import { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
-
-const messages = defineMessages({
- heading: { id: 'column.admin.announcements', defaultMessage: 'Announcements' },
- deleteConfirm: { id: 'confirmations.admin.delete_announcement.confirm', defaultMessage: 'Delete' },
- deleteHeading: { id: 'confirmations.admin.delete_announcement.heading', defaultMessage: 'Delete announcement' },
- deleteMessage: { id: 'confirmations.admin.delete_announcement.message', defaultMessage: 'Are you sure you want to delete the announcement?' },
-});
-
-interface IAnnouncement {
- announcement: AnnouncementEntity
-}
-
-const Announcement: React.FC = ({ announcement }) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const handleEditAnnouncement = (announcement: AnnouncementEntity) => () => {
- dispatch(initAnnouncementModal(announcement));
- };
-
- const handleDeleteAnnouncement = (id: string) => () => {
- dispatch(openModal('CONFIRM', {
- heading: intl.formatMessage(messages.deleteHeading),
- message: intl.formatMessage(messages.deleteMessage),
- confirm: intl.formatMessage(messages.deleteConfirm),
- onConfirm: () => dispatch(deleteAnnouncement(id)),
- }));
- };
-
- return (
-
-
-
- {(announcement.starts_at || announcement.ends_at || announcement.all_day) && (
-
- {announcement.starts_at && (
-
-
-
-
- {' '}
-
-
- )}
- {announcement.ends_at && (
-
-
-
-
- {' '}
-
-
- )}
- {announcement.all_day && (
-
-
-
- )}
-
- )}
-
-
-
-
-
-
- );
-};
-
-const Announcements: React.FC = () => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const announcements = useAppSelector((state) => state.admin_announcements.items);
- const isLoading = useAppSelector((state) => state.admin_announcements.isLoading);
-
- useEffect(() => {
- dispatch(fetchAdminAnnouncements());
- }, []);
-
- const handleCreateAnnouncement = () => {
- dispatch(initAnnouncementModal());
- };
-
- const emptyMessage = ;
-
- return (
-
-
-
-
- {announcements.map((announcement) => (
-
- ))}
-
-
-
- );
-};
-
-export default Announcements;
diff --git a/app/soapbox/features/admin/user-index.tsx b/app/soapbox/features/admin/user-index.tsx
deleted file mode 100644
index f4150b859..000000000
--- a/app/soapbox/features/admin/user-index.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import debounce from 'lodash/debounce';
-import React, { useCallback, useEffect } from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-
-import { expandUserIndex, fetchUserIndex, setUserIndexQuery } from 'soapbox/actions/admin';
-import ScrollableList from 'soapbox/components/scrollable-list';
-import { Column, Input } from 'soapbox/components/ui';
-import AccountContainer from 'soapbox/containers/account-container';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-
-const messages = defineMessages({
- heading: { id: 'column.admin.users', defaultMessage: 'Users' },
- empty: { id: 'admin.user_index.empty', defaultMessage: 'No users found.' },
- searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' },
-});
-
-const UserIndex: React.FC = () => {
- const dispatch = useAppDispatch();
- const intl = useIntl();
-
- const { isLoading, items, total, query, next } = useAppSelector((state) => state.admin_user_index);
-
- const handleLoadMore = () => {
- if (!isLoading) dispatch(expandUserIndex());
- };
-
- const updateQuery = useCallback(debounce(() => {
- dispatch(fetchUserIndex());
- }, 900, { leading: true }), []);
-
- const handleQueryChange: React.ChangeEventHandler = e => {
- dispatch(setUserIndexQuery(e.target.value));
- updateQuery();
- };
-
- useEffect(() => {
- updateQuery();
- }, []);
-
-
- const hasMore = items.count() < total && !!next;
-
- const showLoading = isLoading && items.isEmpty();
-
- return (
-
-
-
- {items.map(id =>
- ,
- )}
-
-
- );
-};
-
-export default UserIndex;
diff --git a/app/soapbox/features/chats/components/__tests__/chat-message-reaction.test.tsx b/app/soapbox/features/chats/components/__tests__/chat-message-reaction.test.tsx
deleted file mode 100644
index 6ab22d4d5..000000000
--- a/app/soapbox/features/chats/components/__tests__/chat-message-reaction.test.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-
-import { render, screen } from '../../../../jest/test-helpers';
-import ChatMessageReaction from '../chat-message-reaction';
-
-const emojiReaction = ({
- name: '👍',
- count: 1,
- me: false,
-});
-
-describe('', () => {
- it('renders properly', () => {
- render(
- ,
- );
-
- expect(screen.getByRole('img').getAttribute('alt')).toEqual(emojiReaction.name);
- expect(screen.getByRole('button')).toHaveTextContent(String(emojiReaction.count));
- });
-
- it('triggers the "onAddReaction" function', async () => {
- const onAddFn = jest.fn();
- const onRemoveFn = jest.fn();
- const user = userEvent.setup();
-
- render(
- ,
- );
-
- expect(onAddFn).not.toBeCalled();
- expect(onRemoveFn).not.toBeCalled();
-
- await user.click(screen.getByRole('button'));
-
- // add function triggered
- expect(onAddFn).toBeCalled();
- expect(onRemoveFn).not.toBeCalled();
- });
-
- it('triggers the "onRemoveReaction" function', async () => {
- const onAddFn = jest.fn();
- const onRemoveFn = jest.fn();
- const user = userEvent.setup();
-
- render(
- ,
- );
-
- expect(onAddFn).not.toBeCalled();
- expect(onRemoveFn).not.toBeCalled();
-
- await user.click(screen.getByRole('button'));
-
- // remove function triggered
- expect(onAddFn).not.toBeCalled();
- expect(onRemoveFn).toBeCalled();
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/chats/components/chat-message-reaction-wrapper/chat-message-reaction-wrapper.tsx b/app/soapbox/features/chats/components/chat-message-reaction-wrapper/chat-message-reaction-wrapper.tsx
deleted file mode 100644
index 91e959f7c..000000000
--- a/app/soapbox/features/chats/components/chat-message-reaction-wrapper/chat-message-reaction-wrapper.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React, { useState, useEffect } from 'react';
-
-import { Portal } from 'soapbox/components/ui';
-import EmojiSelector from 'soapbox/components/ui/emoji-selector/emoji-selector';
-
-interface IChatMessageReactionWrapper {
- onOpen(isOpen: boolean): void
- onSelect(emoji: string): void
- children: JSX.Element
-}
-
-/**
- * Emoji Reaction Selector
- */
-function ChatMessageReactionWrapper(props: IChatMessageReactionWrapper) {
- const { onOpen, onSelect, children } = props;
-
- const [isOpen, setIsOpen] = useState(false);
-
- const [referenceElement, setReferenceElement] = useState(null);
-
- const handleSelect = (emoji: string) => {
- onSelect(emoji);
- setIsOpen(false);
- };
-
- const onToggleVisibility = () => setIsOpen((prevValue) => !prevValue);
-
- useEffect(() => {
- onOpen(isOpen);
- }, [isOpen]);
-
- return (
-
- {React.cloneElement(children, {
- ref: setReferenceElement,
- onClick: onToggleVisibility,
- })}
-
- {isOpen && (
-
- setIsOpen(false)}
- offsetOptions={{ mainAxis: 12, crossAxis: -10 }}
- all={false}
- />
-
- )}
-
- );
-}
-
-export default ChatMessageReactionWrapper;
\ No newline at end of file
diff --git a/app/soapbox/features/chats/components/chat-message-reaction.tsx b/app/soapbox/features/chats/components/chat-message-reaction.tsx
deleted file mode 100644
index 9f7cd3e4f..000000000
--- a/app/soapbox/features/chats/components/chat-message-reaction.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import clsx from 'clsx';
-import React from 'react';
-
-import { Text } from 'soapbox/components/ui';
-import emojify from 'soapbox/features/emoji';
-import { EmojiReaction } from 'soapbox/types/entities';
-
-interface IChatMessageReaction {
- emojiReaction: EmojiReaction
- onRemoveReaction(emoji: string): void
- onAddReaction(emoji: string): void
-}
-
-const ChatMessageReaction = (props: IChatMessageReaction) => {
- const { emojiReaction, onAddReaction, onRemoveReaction } = props;
-
- const isAlreadyReacted = emojiReaction.me;
-
- const handleClick = () => {
- if (isAlreadyReacted) {
- onRemoveReaction(emojiReaction.name);
- } else {
- onAddReaction(emojiReaction.name);
- }
- };
-
- return (
-
- );
-};
-
-export default ChatMessageReaction;
diff --git a/app/soapbox/features/chats/components/chat-message.tsx b/app/soapbox/features/chats/components/chat-message.tsx
deleted file mode 100644
index f8c7898ed..000000000
--- a/app/soapbox/features/chats/components/chat-message.tsx
+++ /dev/null
@@ -1,393 +0,0 @@
-import { useMutation } from '@tanstack/react-query';
-import clsx from 'clsx';
-import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
-import { escape } from 'lodash';
-import React, { useMemo, useState } from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-
-import { openModal } from 'soapbox/actions/modals';
-import { initReport, ReportableEntities } from 'soapbox/actions/reports';
-import DropdownMenu from 'soapbox/components/dropdown-menu';
-import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
-import emojify from 'soapbox/features/emoji';
-import Bundle from 'soapbox/features/ui/components/bundle';
-import { MediaGallery } from 'soapbox/features/ui/util/async-components';
-import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
-import { normalizeAccount } from 'soapbox/normalizers';
-import { ChatKeys, IChat, useChatActions } from 'soapbox/queries/chats';
-import { queryClient } from 'soapbox/queries/client';
-import { stripHTML } from 'soapbox/utils/html';
-import { onlyEmoji } from 'soapbox/utils/rich-content';
-
-import ChatMessageReaction from './chat-message-reaction';
-import ChatMessageReactionWrapper from './chat-message-reaction-wrapper/chat-message-reaction-wrapper';
-
-import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
-import type { IMediaGallery } from 'soapbox/components/media-gallery';
-import type { Account, ChatMessage as ChatMessageEntity } from 'soapbox/types/entities';
-
-const messages = defineMessages({
- copy: { id: 'chats.actions.copy', defaultMessage: 'Copy' },
- delete: { id: 'chats.actions.delete', defaultMessage: 'Delete for both' },
- deleteForMe: { id: 'chats.actions.deleteForMe', defaultMessage: 'Delete for me' },
- more: { id: 'chats.actions.more', defaultMessage: 'More' },
- report: { id: 'chats.actions.report', defaultMessage: 'Report' },
-});
-
-const BIG_EMOJI_LIMIT = 3;
-
-const makeEmojiMap = (record: any) => record.get('emojis', ImmutableList()).reduce((map: ImmutableMap, emoji: ImmutableMap) => {
- return map.set(`:${emoji.get('shortcode')}:`, emoji);
-}, ImmutableMap());
-
-const parsePendingContent = (content: string) => {
- return escape(content).replace(/(?:\r\n|\r|\n)/g, '
');
-};
-
-const parseContent = (chatMessage: ChatMessageEntity) => {
- const content = chatMessage.content || '';
- const pending = chatMessage.pending;
- const deleting = chatMessage.deleting;
- const formatted = (pending && !deleting) ? parsePendingContent(content) : content;
- const emojiMap = makeEmojiMap(chatMessage);
- return emojify(formatted, emojiMap.toJS());
-};
-
-interface IChatMessage {
- chat: IChat
- chatMessage: ChatMessageEntity
-}
-
-const ChatMessage = (props: IChatMessage) => {
- const { chat, chatMessage } = props;
-
- const dispatch = useAppDispatch();
- const features = useFeatures();
- const intl = useIntl();
-
- const me = useAppSelector((state) => state.me);
- const { createReaction, deleteChatMessage, deleteReaction } = useChatActions(chat.id);
-
- const [isReactionSelectorOpen, setIsReactionSelectorOpen] = useState(false);
- const [isMenuOpen, setIsMenuOpen] = useState(false);
-
- const handleDeleteMessage = useMutation((chatMessageId: string) => deleteChatMessage(chatMessageId), {
- onSettled: () => {
- queryClient.invalidateQueries(ChatKeys.chatMessages(chat.id));
- },
- });
-
- const content = parseContent(chatMessage);
- const lastReadMessageDateString = chat.latest_read_message_by_account?.find((latest) => latest.id === chat.account.id)?.date;
- const lastReadMessageTimestamp = lastReadMessageDateString ? new Date(lastReadMessageDateString) : null;
- const isMyMessage = chatMessage.account_id === me;
-
- // did this occur before this time?
- const isRead = isMyMessage
- && lastReadMessageTimestamp
- && lastReadMessageTimestamp >= new Date(chatMessage.created_at);
-
- const isOnlyEmoji = useMemo(() => {
- const hiddenEl = document.createElement('div');
- hiddenEl.innerHTML = content;
- return onlyEmoji(hiddenEl, BIG_EMOJI_LIMIT, false);
- }, []);
-
- const emojiReactionRows = useMemo(() => {
- if (!chatMessage.emoji_reactions) {
- return [];
- }
-
- return chatMessage.emoji_reactions.reduce((rows: any, key: any, index) => {
- return (index % 4 === 0 ? rows.push([key])
- : rows[rows.length - 1].push(key)) && rows;
- }, []);
- }, [chatMessage.emoji_reactions]);
-
- const onOpenMedia = (media: any, index: number) => {
- dispatch(openModal('MEDIA', { media, index }));
- };
-
- const maybeRenderMedia = (chatMessage: ChatMessageEntity) => {
- if (!chatMessage.media_attachments.size) return null;
-
- return (
-
- {(Component: React.FC) => (
-
- )}
-
- );
- };
-
- const handleCopyText = (chatMessage: ChatMessageEntity) => {
- if (navigator.clipboard) {
- const text = stripHTML(chatMessage.content);
- navigator.clipboard.writeText(text);
- }
- };
- const setBubbleRef = (c: HTMLDivElement) => {
- if (!c) return;
- const links = c.querySelectorAll('a[rel="ugc"]');
-
- links.forEach(link => {
- link.classList.add('chat-link');
- link.setAttribute('rel', 'ugc nofollow noopener');
- link.setAttribute('target', '_blank');
- });
- };
-
- const getFormattedTimestamp = (chatMessage: ChatMessageEntity) => {
- return intl.formatDate(new Date(chatMessage.created_at), {
- hour12: false,
- year: 'numeric',
- month: 'short',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- });
- };
-
- const menu = useMemo(() => {
- const menu: IMenu = [];
-
- if (navigator.clipboard && chatMessage.content) {
- menu.push({
- text: intl.formatMessage(messages.copy),
- action: () => handleCopyText(chatMessage),
- icon: require('@tabler/icons/copy.svg'),
- });
- }
-
- if (isMyMessage) {
- menu.push({
- text: intl.formatMessage(messages.delete),
- action: () => handleDeleteMessage.mutate(chatMessage.id),
- icon: require('@tabler/icons/trash.svg'),
- destructive: true,
- });
- } else {
- if (features.reportChats) {
- menu.push({
- text: intl.formatMessage(messages.report),
- action: () => dispatch(initReport(ReportableEntities.CHAT_MESSAGE, normalizeAccount(chat.account) as Account, { chatMessage })),
- icon: require('@tabler/icons/flag.svg'),
- });
- }
- menu.push({
- text: intl.formatMessage(messages.deleteForMe),
- action: () => handleDeleteMessage.mutate(chatMessage.id),
- icon: require('@tabler/icons/trash.svg'),
- destructive: true,
- });
- }
-
- return menu;
- }, [chatMessage, chat]);
-
- return (
-
-
- {features.chatEmojiReactions && (
- createReaction.mutate({ emoji, messageId: chatMessage.id, chatMessage })}
- >
-
-
- )}
-
- {menu.length > 0 && (
- setIsMenuOpen(true)}
- onClose={() => setIsMenuOpen(false)}
- >
-
-
- )}
-
-
-
-
-
- {maybeRenderMedia(chatMessage)}
-
- {content && (
-
-
-
-
-
- )}
-
-
-
- {(chatMessage.emoji_reactions?.length) ? (
-
- {emojiReactionRows?.map((emojiReactionRow: any, idx: number) => (
-
- {emojiReactionRow.map((emojiReaction: any, idx: number) => (
- createReaction.mutate({ emoji, messageId: chatMessage.id, chatMessage })}
- onRemoveReaction={(emoji) => deleteReaction.mutate({ emoji, messageId: chatMessage.id })}
- />
- ))}
-
- ))}
-
- ) : null}
-
-
-
-
-
- {intl.formatTime(chatMessage.created_at)}
-
-
- {(isMyMessage && features.chatsReadReceipts) ? (
- <>
- {isRead ? (
-
-
-
- ) : (
-
-
-
- )}
- >
- ) : null}
-
-
-
-
-
- );
-};
-
-export default ChatMessage;
diff --git a/app/soapbox/features/chats/components/chat-pending-upload.tsx b/app/soapbox/features/chats/components/chat-pending-upload.tsx
deleted file mode 100644
index 373d548ce..000000000
--- a/app/soapbox/features/chats/components/chat-pending-upload.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-
-import { ProgressBar } from 'soapbox/components/ui';
-
-interface IChatPendingUpload {
- progress: number
-}
-
-/** Displays a loading thumbnail for an upload in the chat composer. */
-const ChatPendingUpload: React.FC = ({ progress }) => {
- return (
-
- );
-};
-
-export default ChatPendingUpload;
\ No newline at end of file
diff --git a/app/soapbox/features/chats/components/chat-textarea.tsx b/app/soapbox/features/chats/components/chat-textarea.tsx
deleted file mode 100644
index f6ee67b93..000000000
--- a/app/soapbox/features/chats/components/chat-textarea.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-
-import { HStack, Textarea } from 'soapbox/components/ui';
-import { Attachment } from 'soapbox/types/entities';
-
-import ChatPendingUpload from './chat-pending-upload';
-import ChatUpload from './chat-upload';
-
-interface IChatTextarea extends React.ComponentProps {
- attachments?: Attachment[]
- onDeleteAttachment?: (i: number) => void
- uploadCount?: number
- uploadProgress?: number
-}
-
-/** Custom textarea for chats. */
-const ChatTextarea: React.FC = React.forwardRef(({
- attachments,
- onDeleteAttachment,
- uploadCount = 0,
- uploadProgress = 0,
- ...rest
-}, ref) => {
- const isUploading = uploadCount > 0;
-
- const handleDeleteAttachment = (i: number) => {
- return () => {
- if (onDeleteAttachment) {
- onDeleteAttachment(i);
- }
- };
- };
-
- return (
-
- {(!!attachments?.length || isUploading) && (
-
- {attachments?.map((attachment, i) => (
-
-
-
- ))}
-
- {Array.from(Array(uploadCount)).map(() => (
-
-
-
- ))}
-
- )}
-
-
-
- );
-});
-
-export default ChatTextarea;
diff --git a/app/soapbox/features/chats/components/chat-upload-preview.tsx b/app/soapbox/features/chats/components/chat-upload-preview.tsx
deleted file mode 100644
index a67d470b6..000000000
--- a/app/soapbox/features/chats/components/chat-upload-preview.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react';
-
-import { Icon } from 'soapbox/components/ui';
-import { MIMETYPE_ICONS } from 'soapbox/components/upload';
-
-import type { Attachment } from 'soapbox/types/entities';
-
-const defaultIcon = require('@tabler/icons/paperclip.svg');
-
-interface IChatUploadPreview {
- className?: string
- attachment: Attachment
-}
-
-/**
- * Displays a generic preview for an upload depending on its media type.
- * It fills its container and is expected to be sized by its parent.
- */
-const ChatUploadPreview: React.FC = ({ className, attachment }) => {
- const mimeType = attachment.pleroma.get('mime_type') as string | undefined;
-
- switch (attachment.type) {
- case 'image':
- case 'gifv':
- return (
-
- );
- case 'video':
- return (
-
- );
- default:
- return (
-
-
-
- );
- }
-};
-
-export default ChatUploadPreview;
\ No newline at end of file
diff --git a/app/soapbox/features/chats/components/chat-upload.tsx b/app/soapbox/features/chats/components/chat-upload.tsx
deleted file mode 100644
index 53bb8a1fc..000000000
--- a/app/soapbox/features/chats/components/chat-upload.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import clsx from 'clsx';
-import { List as ImmutableList } from 'immutable';
-import React from 'react';
-
-import { openModal } from 'soapbox/actions/modals';
-import Blurhash from 'soapbox/components/blurhash';
-import { Icon } from 'soapbox/components/ui';
-import { useAppDispatch } from 'soapbox/hooks';
-
-import ChatUploadPreview from './chat-upload-preview';
-
-import type { Attachment } from 'soapbox/types/entities';
-
-interface IChatUpload {
- attachment: Attachment
- onDelete?(): void
-}
-
-/** An attachment uploaded to the chat composer, before sending. */
-const ChatUpload: React.FC = ({ attachment, onDelete }) => {
- const dispatch = useAppDispatch();
- const clickable = attachment.type !== 'unknown';
-
- const handleOpenModal = () => {
- dispatch(openModal('MEDIA', { media: ImmutableList.of(attachment), index: 0 }));
- };
-
- return (
-
- );
-};
-
-interface IRemoveButton {
- onClick?: React.MouseEventHandler
-}
-
-/** Floating button to remove an attachment. */
-const RemoveButton: React.FC = ({ onClick }) => {
- return (
-
- );
-};
-
-export default ChatUpload;
\ No newline at end of file
diff --git a/app/soapbox/features/compose/components/reply-group-indicator.tsx b/app/soapbox/features/compose/components/reply-group-indicator.tsx
deleted file mode 100644
index bc808dc5a..000000000
--- a/app/soapbox/features/compose/components/reply-group-indicator.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React, { useCallback } from 'react';
-import { FormattedMessage } from 'react-intl';
-
-import Link from 'soapbox/components/link';
-import { Text } from 'soapbox/components/ui';
-import { useAppSelector } from 'soapbox/hooks';
-import { Group } from 'soapbox/schemas';
-import { makeGetStatus } from 'soapbox/selectors';
-
-interface IReplyGroupIndicator {
- composeId: string
-}
-
-const ReplyGroupIndicator = (props: IReplyGroupIndicator) => {
- const { composeId } = props;
-
- const getStatus = useCallback(makeGetStatus(), []);
-
- const status = useAppSelector((state) => getStatus(state, { id: state.compose.get(composeId)?.in_reply_to! }));
- const group = status?.group as Group;
-
- if (!group) {
- return null;
- }
-
- return (
-
- ,
- }}
- />
-
- );
-};
-
-export default ReplyGroupIndicator;
\ No newline at end of file
diff --git a/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx b/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx
deleted file mode 100644
index 3f486502e..000000000
--- a/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx
+++ /dev/null
@@ -1,258 +0,0 @@
-import { Map as ImmutableMap } from 'immutable';
-import React, { useEffect, useState, useLayoutEffect } from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-import { createSelector } from 'reselect';
-
-import { useEmoji } from 'soapbox/actions/emojis';
-import { changeSetting } from 'soapbox/actions/settings';
-import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
-import { RootState } from 'soapbox/store';
-
-import { buildCustomEmojis } from '../../emoji';
-import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
-
-import type { Emoji, CustomEmoji, NativeEmoji } from 'soapbox/features/emoji';
-
-let EmojiPicker: any; // load asynchronously
-
-export const messages = defineMessages({
- emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
- emoji_pick: { id: 'emoji_button.pick', defaultMessage: 'Pick an emoji…' },
- emoji_oh_no: { id: 'emoji_button.oh_no', defaultMessage: 'Oh no!' },
- emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
- emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emoji\'s found.' },
- emoji_add_custom: { id: 'emoji_button.add_custom', defaultMessage: 'Add custom emoji' },
- custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
- recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
- search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
- people: { id: 'emoji_button.people', defaultMessage: 'People' },
- nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
- food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
- activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
- travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
- objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
- symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
- flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
- skins_choose: { id: 'emoji_button.skins_choose', defaultMessage: 'Choose default skin tone' },
- skins_1: { id: 'emoji_button.skins_1', defaultMessage: 'Default' },
- skins_2: { id: 'emoji_button.skins_2', defaultMessage: 'Light' },
- skins_3: { id: 'emoji_button.skins_3', defaultMessage: 'Medium-Light' },
- skins_4: { id: 'emoji_button.skins_4', defaultMessage: 'Medium' },
- skins_5: { id: 'emoji_button.skins_5', defaultMessage: 'Medium-Dark' },
- skins_6: { id: 'emoji_button.skins_6', defaultMessage: 'Dark' },
-});
-
-export interface IEmojiPickerDropdown {
- onPickEmoji?: (emoji: Emoji) => void
- condensed?: boolean
- withCustom?: boolean
- visible: boolean
- setVisible: (value: boolean) => void
- update: (() => any) | null
-}
-
-const perLine = 8;
-const lines = 2;
-
-const DEFAULTS = [
- '+1',
- 'grinning',
- 'kissing_heart',
- 'heart_eyes',
- 'laughing',
- 'stuck_out_tongue_winking_eye',
- 'sweat_smile',
- 'joy',
- 'yum',
- 'disappointed',
- 'thinking_face',
- 'weary',
- 'sob',
- 'sunglasses',
- 'heart',
- 'ok_hand',
-];
-
-export const getFrequentlyUsedEmojis = createSelector([
- (state: RootState) => state.settings.get('frequentlyUsedEmojis', ImmutableMap()),
-], (emojiCounters: ImmutableMap) => {
- let emojis = emojiCounters
- .keySeq()
- .sort((a, b) => emojiCounters.get(a)! - emojiCounters.get(b)!)
- .reverse()
- .slice(0, perLine * lines)
- .toArray();
-
- if (emojis.length < DEFAULTS.length) {
- const uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
- emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
- }
-
- return emojis;
-});
-
-const getCustomEmojis = createSelector([
- (state: RootState) => state.custom_emojis,
-], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
- const aShort = a.get('shortcode')!.toLowerCase();
- const bShort = b.get('shortcode')!.toLowerCase();
-
- if (aShort < bShort) {
- return -1;
- } else if (aShort > bShort) {
- return 1;
- } else {
- return 0;
- }
-}));
-
-// Fixes render bug where popover has a delayed position update
-const RenderAfter = ({ children, update }: any) => {
- const [nextTick, setNextTick] = useState(false);
-
- useEffect(() => {
- setTimeout(() => {
- setNextTick(true);
- }, 0);
- }, []);
-
- useLayoutEffect(() => {
- if (nextTick) {
- update();
- }
- }, [nextTick, update]);
-
- return nextTick ? children : null;
-};
-
-const EmojiPickerDropdown: React.FC = ({
- onPickEmoji, visible, setVisible, update, withCustom = true,
-}) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
- const settings = useSettings();
- const title = intl.formatMessage(messages.emoji);
- const userTheme = settings.get('themeMode');
- const theme = (userTheme === 'dark' || userTheme === 'light') ? userTheme : 'auto';
-
- const customEmojis = useAppSelector((state) => getCustomEmojis(state));
- const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
-
- const [loading, setLoading] = useState(false);
-
- const handlePick = (emoji: any) => {
- setVisible(false);
-
- let pickedEmoji: Emoji;
-
- if (emoji.native) {
- pickedEmoji = {
- id: emoji.id,
- colons: emoji.shortcodes,
- custom: false,
- native: emoji.native,
- unified: emoji.unified,
- } as NativeEmoji;
- } else {
- pickedEmoji = {
- id: emoji.id,
- colons: emoji.shortcodes,
- custom: true,
- imageUrl: emoji.src,
- } as CustomEmoji;
- }
-
- dispatch(useEmoji(pickedEmoji)); // eslint-disable-line react-hooks/rules-of-hooks
-
- if (onPickEmoji) {
- onPickEmoji(pickedEmoji);
- }
- };
-
- const handleSkinTone = (skinTone: string) => {
- dispatch(changeSetting(['skinTone'], skinTone));
- };
-
- const getI18n = () => {
- return {
- search: intl.formatMessage(messages.emoji_search),
- pick: intl.formatMessage(messages.emoji_pick),
- search_no_results_1: intl.formatMessage(messages.emoji_oh_no),
- search_no_results_2: intl.formatMessage(messages.emoji_not_found),
- add_custom: intl.formatMessage(messages.emoji_add_custom),
- categories: {
- search: intl.formatMessage(messages.search_results),
- frequent: intl.formatMessage(messages.recent),
- people: intl.formatMessage(messages.people),
- nature: intl.formatMessage(messages.nature),
- foods: intl.formatMessage(messages.food),
- activity: intl.formatMessage(messages.activity),
- places: intl.formatMessage(messages.travel),
- objects: intl.formatMessage(messages.objects),
- symbols: intl.formatMessage(messages.symbols),
- flags: intl.formatMessage(messages.flags),
- custom: intl.formatMessage(messages.custom),
- },
- skins: {
- choose: intl.formatMessage(messages.skins_choose),
- 1: intl.formatMessage(messages.skins_1),
- 2: intl.formatMessage(messages.skins_2),
- 3: intl.formatMessage(messages.skins_3),
- 4: intl.formatMessage(messages.skins_4),
- 5: intl.formatMessage(messages.skins_5),
- 6: intl.formatMessage(messages.skins_6),
- },
- };
- };
-
- useEffect(() => {
- // fix scrolling focus issue
- if (visible) {
- document.body.style.overflow = 'hidden';
- } else {
- document.body.style.overflow = '';
- }
-
- if (!EmojiPicker) {
- setLoading(true);
-
- EmojiPickerAsync().then(EmojiMart => {
- EmojiPicker = EmojiMart.Picker;
-
- setLoading(false);
- }).catch(() => {
- setLoading(false);
- });
- }
- }, [visible]);
-
- useEffect(() => () => {
- document.body.style.overflow = '';
- }, []);
-
- return (
- visible ? (
-
- {!loading && (
-
- )}
-
- ) : null
- );
-};
-
-export default EmojiPickerDropdown;
diff --git a/app/soapbox/features/emoji/components/emoji-picker.tsx b/app/soapbox/features/emoji/components/emoji-picker.tsx
deleted file mode 100644
index 695fae2cc..000000000
--- a/app/soapbox/features/emoji/components/emoji-picker.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Picker as EmojiPicker } from 'emoji-mart';
-import React, { useRef, useEffect } from 'react';
-
-import { joinPublicPath } from 'soapbox/utils/static';
-
-import data from '../data';
-
-const getSpritesheetURL = (set: string) => {
- return require('emoji-datasource/img/twitter/sheets/32.png');
-};
-
-const getImageURL = (set: string, name: string) => {
- return joinPublicPath(`/packs/emoji/${name}.svg`);
-};
-
-const Picker = (props: any) => {
- const ref = useRef(null);
-
- useEffect(() => {
- const input = { ...props, data, ref, getImageURL, getSpritesheetURL };
-
- new EmojiPicker(input);
- }, []);
-
- return ;
-};
-
-export {
- Picker,
-};
diff --git a/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx b/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx
deleted file mode 100644
index 5929d1711..000000000
--- a/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useFloating, shift } from '@floating-ui/react';
-import clsx from 'clsx';
-import React, { KeyboardEvent, useState } from 'react';
-import { createPortal } from 'react-dom';
-import { defineMessages, useIntl } from 'react-intl';
-
-import { IconButton } from 'soapbox/components/ui';
-import { useClickOutside } from 'soapbox/hooks';
-
-import EmojiPickerDropdown, { IEmojiPickerDropdown } from '../components/emoji-picker-dropdown';
-
-export const messages = defineMessages({
- emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
-});
-
-const EmojiPickerDropdownContainer = (
- props: Pick,
-) => {
- const intl = useIntl();
- const title = intl.formatMessage(messages.emoji);
-
- const [visible, setVisible] = useState(false);
-
- const { x, y, strategy, refs, update } = useFloating({
- middleware: [shift()],
- });
-
- useClickOutside(refs, () => {
- setVisible(false);
- });
-
- const handleToggle = (e: MouseEvent | KeyboardEvent) => {
- e.stopPropagation();
- setVisible(!visible);
- };
-
- return (
-
-
}
- tabIndex={0}
- />
-
- {createPortal(
-
-
-
,
- document.body,
- )}
-
- );
-};
-
-
-export default EmojiPickerDropdownContainer;
diff --git a/app/soapbox/features/emoji/data.ts b/app/soapbox/features/emoji/data.ts
deleted file mode 100644
index 2d74822f3..000000000
--- a/app/soapbox/features/emoji/data.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import data from '@emoji-mart/data/sets/14/twitter.json';
-
-export interface NativeEmoji {
- unified: string
- native: string
- x: number
- y: number
-}
-
-export interface CustomEmoji {
- src: string
-}
-
-export interface Emoji {
- id: string
- name: string
- keywords: string[]
- skins: T[]
- version?: number
-}
-
-export interface EmojiCategory {
- id: string
- emojis: string[]
-}
-
-export interface EmojiMap {
- [s: string]: Emoji
-}
-
-export interface EmojiAlias {
- [s: string]: string
-}
-
-export interface EmojiSheet {
- cols: number
- rows: number
-}
-
-export interface EmojiData {
- categories: EmojiCategory[]
- emojis: EmojiMap
- aliases: EmojiAlias
- sheet: EmojiSheet
-}
-
-const emojiData = data as EmojiData;
-const { categories, emojis, aliases, sheet } = emojiData;
-
-export { categories, emojis, aliases, sheet };
-
-export default emojiData;
diff --git a/app/soapbox/features/emoji/index.ts b/app/soapbox/features/emoji/index.ts
deleted file mode 100644
index 61a957e9e..000000000
--- a/app/soapbox/features/emoji/index.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-import split from 'graphemesplit';
-
-import unicodeMapping from './mapping';
-
-import type { Emoji as EmojiMart, CustomEmoji as EmojiMartCustom } from 'soapbox/features/emoji/data';
-
-/*
- * TODO: Consolate emoji object types
- *
- * There are five different emoji objects currently
- * - emoji-mart's "onPickEmoji" handler
- * - emoji-mart's custom emoji types
- * - an Emoji type that is either NativeEmoji or CustomEmoji
- * - a type inside redux's `store.custom_emoji` immutablejs
- *
- * there needs to be one type for the picker handler callback
- * and one type for the emoji-mart data
- * and one type that is used everywhere that the above two are converted into
- */
-
-export interface CustomEmoji {
- id: string
- colons: string
- custom: true
- imageUrl: string
-}
-
-export interface NativeEmoji {
- id: string
- colons: string
- custom?: false
- unified: string
- native: string
-}
-
-export type Emoji = CustomEmoji | NativeEmoji;
-
-export function isCustomEmoji(emoji: Emoji): emoji is CustomEmoji {
- return (emoji as CustomEmoji).imageUrl !== undefined;
-}
-
-export function isNativeEmoji(emoji: Emoji): emoji is NativeEmoji {
- return (emoji as NativeEmoji).native !== undefined;
-}
-
-const isAlphaNumeric = (c: string) => {
- const code = c.charCodeAt(0);
-
- if (!(code > 47 && code < 58) && // numeric (0-9)
- !(code > 64 && code < 91) && // upper alpha (A-Z)
- !(code > 96 && code < 123)) { // lower alpha (a-z)
- return false;
- } else {
- return true;
- }
-};
-
-const validEmojiChar = (c: string) => {
- return isAlphaNumeric(c)
- || c === '_'
- || c === '-'
- || c === '.';
-};
-
-const convertCustom = (shortname: string, filename: string) => {
- return ``;
-};
-
-const convertUnicode = (c: string) => {
- const { unified, shortcode } = unicodeMapping[c];
-
- return ``;
-};
-
-const convertEmoji = (str: string, customEmojis: any) => {
- if (str.length < 3) return str;
- if (str in customEmojis) {
- const emoji = customEmojis[str];
- const filename = emoji.static_url;
-
- if (filename?.length > 0) {
- return convertCustom(str, filename);
- }
- }
-
- return str;
-};
-
-export const emojifyText = (str: string, customEmojis = {}) => {
- let buf = '';
- let stack = '';
- let open = false;
-
- const clearStack = () => {
- buf += stack;
- open = false;
- stack = '';
- };
-
- for (let c of split(str)) {
- // convert FE0E selector to FE0F so it can be found in unimap
- if (c.codePointAt(c.length - 1) === 65038) {
- c = c.slice(0, -1) + String.fromCodePoint(65039);
- }
-
- // unqualified emojis aren't in emoji-mart's mappings so we just add FEOF
- const unqualified = c + String.fromCodePoint(65039);
-
- if (c in unicodeMapping) {
- if (open) { // unicode emoji inside colon
- clearStack();
- }
-
- buf += convertUnicode(c);
- } else if (unqualified in unicodeMapping) {
- if (open) { // unicode emoji inside colon
- clearStack();
- }
-
- buf += convertUnicode(unqualified);
- } else if (c === ':') {
- stack += ':';
-
- // we see another : we convert it and clear the stack buffer
- if (open) {
- buf += convertEmoji(stack, customEmojis);
- stack = '';
- }
-
- open = !open;
- } else {
- if (open) {
- stack += c;
-
- // if the stack is non-null and we see invalid chars it's a string not emoji
- // so we push it to the return result and clear it
- if (!validEmojiChar(c)) {
- clearStack();
- }
- } else {
- buf += c;
- }
- }
- }
-
- // never found a closing colon so it's just a raw string
- if (open) {
- buf += stack;
- }
-
- return buf;
-};
-
-export const parseHTML = (str: string): { text: boolean, data: string }[] => {
- const tokens = [];
- let buf = '';
- let stack = '';
- let open = false;
-
- for (const c of str) {
- if (c === '<') {
- if (open) {
- tokens.push({ text: true, data: stack });
- stack = '<';
- } else {
- tokens.push({ text: true, data: buf });
- stack = '<';
- open = true;
- }
- } else if (c === '>') {
- if (open) {
- open = false;
- tokens.push({ text: false, data: stack + '>' });
- stack = '';
- buf = '';
- } else {
- buf += '>';
- }
-
- } else {
- if (open) {
- stack += c;
- } else {
- buf += c;
- }
- }
- }
-
- if (open) {
- tokens.push({ text: true, data: buf + stack });
- } else if (buf !== '') {
- tokens.push({ text: true, data: buf });
- }
-
- return tokens;
-};
-
-const emojify = (str: string, customEmojis = {}) => {
- return parseHTML(str)
- .map(({ text, data }) => {
- if (!text) return data;
- if (data.length === 0 || data === ' ') return data;
-
- return emojifyText(data, customEmojis);
- })
- .join('');
-};
-
-export default emojify;
-
-export const buildCustomEmojis = (customEmojis: any) => {
- const emojis: EmojiMart[] = [];
-
- customEmojis.forEach((emoji: any) => {
- const shortcode = emoji.get('shortcode');
- const url = emoji.get('static_url');
- const name = shortcode.replace(':', '');
-
- emojis.push({
- id: name,
- name,
- keywords: [name],
- skins: [{ src: url }],
- });
- });
-
- return emojis;
-};
diff --git a/app/soapbox/features/emoji/mapping.ts b/app/soapbox/features/emoji/mapping.ts
deleted file mode 100644
index 5259c2dc2..000000000
--- a/app/soapbox/features/emoji/mapping.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import data, { EmojiData } from './data';
-
-const stripLeadingZeros = /^0+/;
-
-function replaceAll(str: string, find: string, replace: string) {
- return str.replace(new RegExp(find, 'g'), replace);
-}
-
-interface UnicodeMap {
- [s: string]: {
- unified: string
- shortcode: string
- }
-}
-
-/*
- * Twemoji strips their hex codes from unicode codepoints to make it look "pretty"
- * - leading 0s are removed
- * - fe0f is removed unless it has 200d
- * - fe0f is NOT removed for 1f441-fe0f-200d-1f5e8-fe0f even though it has a 200d
- *
- * this is all wrong
- */
-
-const blacklist = {
- '1f441-fe0f-200d-1f5e8-fe0f': true,
-};
-
-const tweaks = {
- '#⃣': ['23-20e3', 'hash'],
- '*⃣': ['2a-20e3', 'keycap_star'],
- '0⃣': ['30-20e3', 'zero'],
- '1⃣': ['31-20e3', 'one'],
- '2⃣': ['32-20e3', 'two'],
- '3⃣': ['33-20e3', 'three'],
- '4⃣': ['34-20e3', 'four'],
- '5⃣': ['35-20e3', 'five'],
- '6⃣': ['36-20e3', 'six'],
- '7⃣': ['37-20e3', 'seven'],
- '8⃣': ['38-20e3', 'eight'],
- '9⃣': ['39-20e3', 'nine'],
- '❤🔥': ['2764-fe0f-200d-1f525', 'heart_on_fire'],
- '❤🩹': ['2764-fe0f-200d-1fa79', 'mending_heart'],
- '👁🗨️': ['1f441-fe0f-200d-1f5e8-fe0f', 'eye-in-speech-bubble'],
- '👁️🗨': ['1f441-fe0f-200d-1f5e8-fe0f', 'eye-in-speech-bubble'],
- '👁🗨': ['1f441-fe0f-200d-1f5e8-fe0f', 'eye-in-speech-bubble'],
- '🕵♂️': ['1f575-fe0f-200d-2642-fe0f', 'male-detective'],
- '🕵️♂': ['1f575-fe0f-200d-2642-fe0f', 'male-detective'],
- '🕵♂': ['1f575-fe0f-200d-2642-fe0f', 'male-detective'],
- '🕵♀️': ['1f575-fe0f-200d-2640-fe0f', 'female-detective'],
- '🕵️♀': ['1f575-fe0f-200d-2640-fe0f', 'female-detective'],
- '🕵♀': ['1f575-fe0f-200d-2640-fe0f', 'female-detective'],
- '🏌♂️': ['1f3cc-fe0f-200d-2642-fe0f', 'man-golfing'],
- '🏌️♂': ['1f3cc-fe0f-200d-2642-fe0f', 'man-golfing'],
- '🏌♂': ['1f3cc-fe0f-200d-2642-fe0f', 'man-golfing'],
- '🏌♀️': ['1f3cc-fe0f-200d-2640-fe0f', 'woman-golfing'],
- '🏌️♀': ['1f3cc-fe0f-200d-2640-fe0f', 'woman-golfing'],
- '🏌♀': ['1f3cc-fe0f-200d-2640-fe0f', 'woman-golfing'],
- '⛹♂️': ['26f9-fe0f-200d-2642-fe0f', 'man-bouncing-ball'],
- '⛹️♂': ['26f9-fe0f-200d-2642-fe0f', 'man-bouncing-ball'],
- '⛹♂': ['26f9-fe0f-200d-2642-fe0f', 'man-bouncing-ball'],
- '⛹♀️': ['26f9-fe0f-200d-2640-fe0f', 'woman-bouncing-ball'],
- '⛹️♀': ['26f9-fe0f-200d-2640-fe0f', 'woman-bouncing-ball'],
- '⛹♀': ['26f9-fe0f-200d-2640-fe0f', 'woman-bouncing-ball'],
- '🏋♂️': ['1f3cb-fe0f-200d-2642-fe0f', 'man-lifting-weights'],
- '🏋️♂': ['1f3cb-fe0f-200d-2642-fe0f', 'man-lifting-weights'],
- '🏋♂': ['1f3cb-fe0f-200d-2642-fe0f', 'man-lifting-weights'],
- '🏋♀️': ['1f3cb-fe0f-200d-2640-fe0f', 'woman-lifting-weights'],
- '🏋️♀': ['1f3cb-fe0f-200d-2640-fe0f', 'woman-lifting-weights'],
- '🏋♀': ['1f3cb-fe0f-200d-2640-fe0f', 'woman-lifting-weights'],
- '🏳🌈': ['1f3f3-fe0f-200d-1f308', 'rainbow_flag'],
- '🏳⚧️': ['1f3f3-fe0f-200d-26a7-fe0f', 'transgender_flag'],
- '🏳️⚧': ['1f3f3-fe0f-200d-26a7-fe0f', 'transgender_flag'],
- '🏳⚧': ['1f3f3-fe0f-200d-26a7-fe0f', 'transgender_flag'],
-};
-
-const stripcodes = (unified: string, native: string) => {
- const stripped = unified.replace(stripLeadingZeros, '');
-
- if (unified.includes('200d') && !(unified in blacklist)) {
- return stripped;
- } else {
- return replaceAll(stripped, '-fe0f', '');
- }
-};
-
-export const generateMappings = (data: EmojiData): UnicodeMap => {
- const result: UnicodeMap = {};
- const emojis = Object.values(data.emojis ?? {});
-
- for (const value of emojis) {
- for (const item of value.skins) {
- const { unified, native } = item;
- const stripped = stripcodes(unified, native);
-
- result[native] = { unified: stripped, shortcode: value.id };
- }
- }
-
- for (const [native, [unified, shortcode]] of Object.entries(tweaks)) {
- const stripped = stripcodes(unified, native);
-
- result[native] = { unified: stripped, shortcode };
- }
-
- return result;
-};
-
-const unicodeMapping = generateMappings(data);
-
-export default unicodeMapping;
diff --git a/app/soapbox/features/emoji/search.ts b/app/soapbox/features/emoji/search.ts
deleted file mode 100644
index dbcb29756..000000000
--- a/app/soapbox/features/emoji/search.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { Index } from 'flexsearch-ts';
-import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
-
-import data from './data';
-
-import type { Emoji } from './index';
-
-const index = new Index({
- tokenize: 'full',
- optimize: true,
- context: true,
-});
-
-for (const [key, emoji] of Object.entries(data.emojis)) {
- index.add('n' + key, emoji.name);
-}
-
-export interface searchOptions {
- maxResults?: number
- custom?: any
-}
-
-export const addCustomToPool = (customEmojis: any[]) => {
- // @ts-ignore
- for (const key in index.register) {
- if (key[0] === 'c') {
- index.remove(key); // remove old custom emojis
- }
- }
-
- let i = 0;
-
- for (const emoji of customEmojis) {
- index.add('c' + i++, emoji.id);
- }
-};
-
-// we can share an index by prefixing custom emojis with 'c' and native with 'n'
-const search = (
- str: string, { maxResults = 5 }: searchOptions = {},
- custom_emojis?: ImmutableList>,
-): Emoji[] => {
- return index.search(str, maxResults)
- .flatMap((id) => {
- if (typeof id !== 'string') return;
-
- if (id[0] === 'c' && custom_emojis) {
- const index = Number(id.slice(1));
- const custom = custom_emojis.get(index);
-
- if (custom) {
- return {
- id: custom.get('shortcode', ''),
- colons: ':' + custom.get('shortcode', '') + ':',
- custom: true,
- imageUrl: custom.get('static_url', ''),
- };
- }
- }
-
- const skins = data.emojis[id.slice(1)]?.skins;
-
- if (skins) {
- return {
- id: id.slice(1),
- colons: ':' + id.slice(1) + ':',
- unified: skins[0].unified,
- native: skins[0].native,
- };
- }
- }).filter(Boolean) as Emoji[];
-};
-
-export default search;
diff --git a/app/soapbox/features/filters/edit-filter.tsx b/app/soapbox/features/filters/edit-filter.tsx
deleted file mode 100644
index 9fd5363fa..000000000
--- a/app/soapbox/features/filters/edit-filter.tsx
+++ /dev/null
@@ -1,279 +0,0 @@
-import React, { useEffect, useMemo, useState } from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-import { useHistory } from 'react-router-dom';
-
-import { createFilter, fetchFilter, updateFilter } from 'soapbox/actions/filters';
-import List, { ListItem } from 'soapbox/components/list';
-import MissingIndicator from 'soapbox/components/missing-indicator';
-import { Button, Column, Form, FormActions, FormGroup, HStack, Input, Stack, Streamfield, Text, Toggle } from 'soapbox/components/ui';
-import { useAppDispatch, useFeatures } from 'soapbox/hooks';
-import { normalizeFilter } from 'soapbox/normalizers';
-import toast from 'soapbox/toast';
-
-import { SelectDropdown } from '../forms';
-
-import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
-
-interface IFilterField {
- id?: string
- keyword: string
- whole_word: boolean
- _destroy?: boolean
-}
-
-interface IEditFilter {
- params: { id?: string }
-}
-
-const messages = defineMessages({
- subheading_add_new: { id: 'column.filters.subheading_add_new', defaultMessage: 'Add New Filter' },
- title: { id: 'column.filters.title', defaultMessage: 'Title' },
- keyword: { id: 'column.filters.keyword', defaultMessage: 'Keyword or phrase' },
- keywords: { id: 'column.filters.keywords', defaultMessage: 'Keywords or phrases' },
- expires: { id: 'column.filters.expires', defaultMessage: 'Expire after' },
- home_timeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' },
- public_timeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' },
- notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' },
- conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' },
- accounts: { id: 'column.filters.accounts', defaultMessage: 'Accounts' },
- drop_header: { id: 'column.filters.drop_header', defaultMessage: 'Drop instead of hide' },
- drop_hint: { id: 'column.filters.drop_hint', defaultMessage: 'Filtered posts will disappear irreversibly, even if filter is later removed' },
- hide_header: { id: 'column.filters.hide_header', defaultMessage: 'Hide completely' },
- hide_hint: { id: 'column.filters.hide_hint', defaultMessage: 'Completely hide the filtered content, instead of showing a warning' },
- add_new: { id: 'column.filters.add_new', defaultMessage: 'Add New Filter' },
- edit: { id: 'column.filters.edit', defaultMessage: 'Edit Filter' },
- create_error: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' },
- expiration_never: { id: 'colum.filters.expiration.never', defaultMessage: 'Never' },
- expiration_1800: { id: 'colum.filters.expiration.1800', defaultMessage: '30 minutes' },
- expiration_3600: { id: 'colum.filters.expiration.3600', defaultMessage: '1 hour' },
- expiration_21600: { id: 'colum.filters.expiration.21600', defaultMessage: '6 hours' },
- expiration_43200: { id: 'colum.filters.expiration.43200', defaultMessage: '12 hours' },
- expiration_86400: { id: 'colum.filters.expiration.86400', defaultMessage: '1 day' },
- expiration_604800: { id: 'colum.filters.expiration.604800', defaultMessage: '1 week' },
-});
-
-const FilterField: StreamfieldComponent = ({ value, onChange }) => {
- const intl = useIntl();
-
- const handleChange = (key: string): React.ChangeEventHandler =>
- e => onChange({ ...value, [key]: e.currentTarget[e.currentTarget.type === 'checkbox' ? 'checked' : 'value'] });
-
- return (
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-const EditFilter: React.FC = ({ params }) => {
- const intl = useIntl();
- const history = useHistory();
- const dispatch = useAppDispatch();
- const features = useFeatures();
-
- const [loading, setLoading] = useState(false);
- const [notFound, setNotFound] = useState(false);
-
- const [title, setTitle] = useState('');
- const [expiresIn, setExpiresIn] = useState(null);
- const [homeTimeline, setHomeTimeline] = useState(true);
- const [publicTimeline, setPublicTimeline] = useState(false);
- const [notifications, setNotifications] = useState(false);
- const [conversations, setConversations] = useState(false);
- const [accounts, setAccounts] = useState(false);
- const [hide, setHide] = useState(false);
- const [keywords, setKeywords] = useState([{ keyword: '', whole_word: false }]);
-
- const expirations = useMemo(() => ({
- '': intl.formatMessage(messages.expiration_never),
- 1800: intl.formatMessage(messages.expiration_1800),
- 3600: intl.formatMessage(messages.expiration_3600),
- 21600: intl.formatMessage(messages.expiration_21600),
- 43200: intl.formatMessage(messages.expiration_43200),
- 86400: intl.formatMessage(messages.expiration_86400),
- 604800: intl.formatMessage(messages.expiration_604800),
- }), []);
-
- const handleSelectChange: React.ChangeEventHandler = e => {
- setExpiresIn(e.target.value);
- };
-
- const handleAddNew: React.FormEventHandler = e => {
- e.preventDefault();
- const context: Array = [];
-
- if (homeTimeline) {
- context.push('home');
- }
- if (publicTimeline) {
- context.push('public');
- }
- if (notifications) {
- context.push('notifications');
- }
- if (conversations) {
- context.push('thread');
- }
- if (accounts) {
- context.push('account');
- }
-
- dispatch(params.id
- ? updateFilter(params.id, title, expiresIn, context, hide, keywords)
- : createFilter(title, expiresIn, context, hide, keywords)).then(() => {
- history.push('/filters');
- }).catch(() => {
- toast.error(intl.formatMessage(messages.create_error));
- });
- };
-
- const handleChangeKeyword = (keywords: { keyword: string, whole_word: boolean }[]) => setKeywords(keywords);
-
- const handleAddKeyword = () => setKeywords(keywords => [...keywords, { keyword: '', whole_word: false }]);
-
- const handleRemoveKeyword = (i: number) => setKeywords(keywords => keywords[i].id
- ? keywords.map((keyword, index) => index === i ? { ...keyword, _destroy: true } : keyword)
- : keywords.filter((_, index) => index !== i));
-
- useEffect(() => {
- if (params.id) {
- setLoading(true);
- dispatch(fetchFilter(params.id))?.then((res: any) => {
- if (res.filter) {
- const filter = normalizeFilter(res.filter);
-
- setTitle(filter.title);
- setHomeTimeline(filter.context.includes('home'));
- setPublicTimeline(filter.context.includes('public'));
- setNotifications(filter.context.includes('notifications'));
- setConversations(filter.context.includes('thread'));
- setAccounts(filter.context.includes('account'));
- setHide(filter.filter_action === 'hide');
- setKeywords(filter.keywords.toJS());
- } else {
- setNotFound(true);
- }
- setLoading(false);
- });
- }
- }, [params.id]);
-
- if (notFound) return ;
-
- return (
-
-
-
- );
-};
-
-export default EditFilter;
diff --git a/app/soapbox/features/followed_tags/index.tsx b/app/soapbox/features/followed_tags/index.tsx
deleted file mode 100644
index 6745f5fc0..000000000
--- a/app/soapbox/features/followed_tags/index.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import debounce from 'lodash/debounce';
-import React, { useEffect } from 'react';
-import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
-
-import { fetchFollowedHashtags, expandFollowedHashtags } from 'soapbox/actions/tags';
-import Hashtag from 'soapbox/components/hashtag';
-import ScrollableList from 'soapbox/components/scrollable-list';
-import { Column } from 'soapbox/components/ui';
-import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder-hashtag';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-
-const messages = defineMessages({
- heading: { id: 'column.followed_tags', defaultMessage: 'Followed hashtags' },
-});
-
-const handleLoadMore = debounce((dispatch) => {
- dispatch(expandFollowedHashtags());
-}, 300, { leading: true });
-
-const FollowedTags = () => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- useEffect(() => {
- dispatch(fetchFollowedHashtags());
- }, []);
-
- const tags = useAppSelector((state => state.followed_tags.items));
- const isLoading = useAppSelector((state => state.followed_tags.isLoading));
- const hasMore = useAppSelector((state => !!state.followed_tags.next));
-
- const emptyMessage = ;
-
- return (
-
- handleLoadMore(dispatch)}
- placeholderComponent={PlaceholderHashtag}
- placeholderCount={5}
- itemClassName='pb-3'
- >
- {tags.map(tag => )}
-
-
- );
-};
-
-export default FollowedTags;
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
deleted file mode 100644
index a0df6affe..000000000
--- a/app/soapbox/features/group/components/__tests__/group-action-button.test.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-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';
-
-let group: Group;
-
-describe('', () => {
- describe('with no group relationship', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: null,
- });
- });
-
- describe('with a private group', () => {
- beforeEach(() => {
- group = { ...group, locked: true };
- });
-
- it('should render the Request Access button', () => {
- render();
-
- expect(screen.getByRole('button')).toHaveTextContent('Request Access');
- });
- });
-
- describe('with a public group', () => {
- beforeEach(() => {
- group = { ...group, locked: false };
- });
-
- it('should render the Join Group button', () => {
- render();
-
- expect(screen.getByRole('button')).toHaveTextContent('Join Group');
- });
- });
- });
-
- describe('with no group relationship member', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- member: false,
- }),
- });
- });
-
- describe('with a private group', () => {
- beforeEach(() => {
- group = { ...group, locked: true };
- });
-
- it('should render the Request Access button', () => {
- render();
-
- expect(screen.getByRole('button')).toHaveTextContent('Request Access');
- });
- });
-
- describe('with a public group', () => {
- beforeEach(() => {
- group = { ...group, locked: false };
- });
-
- it('should render the Join Group button', () => {
- render();
-
- expect(screen.getByRole('button')).toHaveTextContent('Join Group');
- });
- });
- });
-
- describe('when the user has requested to join', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: true,
- member: true,
- }),
- });
- });
-
- it('should render the Cancel Request button', () => {
- render();
-
- expect(screen.getByRole('button')).toHaveTextContent('Cancel Request');
- });
- });
-
- describe('when the user is an Admin', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: false,
- member: true,
- role: GroupRoles.OWNER,
- }),
- });
- });
-
- it('should render the Manage Group button', () => {
- render();
-
- expect(screen.getByRole('button')).toHaveTextContent('Manage Group');
- });
- });
-
- describe('when the user is just a member', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: false,
- member: true,
- role: GroupRoles.USER,
- }),
- });
- });
-
- it('should render the Leave Group button', () => {
- render();
-
- expect(screen.getByRole('button')).toHaveTextContent('Leave Group');
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/__tests__/group-header.test.tsx b/app/soapbox/features/group/components/__tests__/group-header.test.tsx
deleted file mode 100644
index 03f171e14..000000000
--- a/app/soapbox/features/group/components/__tests__/group-header.test.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-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('', () => {
- describe('without a group', () => {
- it('should render the blankslate', () => {
- render();
- 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();
-
- 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();
-
- 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();
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/__tests__/group-member-count.test.tsx b/app/soapbox/features/group/components/__tests__/group-member-count.test.tsx
deleted file mode 100644
index c6e31f8a8..000000000
--- a/app/soapbox/features/group/components/__tests__/group-member-count.test.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-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 GroupMemberCount from '../group-member-count';
-
-let group: Group;
-
-describe('', () => {
- describe('with support for "members_count"', () => {
- describe('with 1 member', () => {
- beforeEach(() => {
- group = buildGroup({
- members_count: 1,
- });
- });
-
- it('should render correctly', () => {
- render();
-
- expect(screen.getByTestId('group-member-count').textContent).toEqual('1 member');
- });
- });
-
- describe('with 2 members', () => {
- beforeEach(() => {
- group = buildGroup({
- members_count: 2,
- });
- });
-
- it('should render correctly', () => {
- render();
-
- expect(screen.getByTestId('group-member-count').textContent).toEqual('2 members');
- });
- });
-
- describe('with 1000 members', () => {
- beforeEach(() => {
- group = buildGroup({
- members_count: 1000,
- });
- });
-
- it('should render correctly', () => {
- render();
-
- expect(screen.getByTestId('group-member-count').textContent).toEqual('1k members');
- });
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx b/app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx
deleted file mode 100644
index abecc3287..000000000
--- a/app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx
+++ /dev/null
@@ -1,320 +0,0 @@
-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('', () => {
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- await waitFor(async() => {
- expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
- });
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/__tests__/group-options-button.test.tsx b/app/soapbox/features/group/components/__tests__/group-options-button.test.tsx
deleted file mode 100644
index e3171bb81..000000000
--- a/app/soapbox/features/group/components/__tests__/group-options-button.test.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-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 GroupOptionsButton from '../group-options-button';
-
-let group: Group;
-
-describe('', () => {
- describe('when the user blocked', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: false,
- member: true,
- blocked_by: true,
- role: GroupRoles.USER,
- }),
- });
- });
-
- it('should render null', () => {
- render();
-
- expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(0);
- });
- });
-
- describe('when the user is an admin', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: false,
- member: true,
- role: GroupRoles.ADMIN,
- }),
- });
- });
-
- it('should render one option for leaving the group', () => {
- render();
-
- // Leave group option only
- expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(1);
- });
- });
-
- describe('when the user is an owner', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: false,
- member: true,
- role: GroupRoles.OWNER,
- }),
- });
- });
-
- it('should render null', () => {
- render();
-
- expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(0);
- });
- });
-
- describe('when the user is a member', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: false,
- member: true,
- role: GroupRoles.USER,
- }),
- });
- });
-
- it('should render the dropdown menu', () => {
- render();
-
- expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(1);
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/__tests__/group-privacy.test.tsx b/app/soapbox/features/group/components/__tests__/group-privacy.test.tsx
deleted file mode 100644
index 3a2d4d9eb..000000000
--- a/app/soapbox/features/group/components/__tests__/group-privacy.test.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-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 GroupPrivacy from '../group-privacy';
-
-let group: Group;
-
-describe('', () => {
- describe('with a Private group', () => {
- beforeEach(() => {
- group = buildGroup({
- locked: true,
- });
- });
-
- it('should render the correct text', () => {
- render();
-
- expect(screen.getByTestId('group-privacy')).toHaveTextContent('Private');
- });
- });
-
- describe('with a Public group', () => {
- beforeEach(() => {
- group = buildGroup({
- locked: false,
- });
- });
-
- it('should render the correct text', () => {
- render();
-
- expect(screen.getByTestId('group-privacy')).toHaveTextContent('Public');
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/__tests__/group-relationship.test.tsx b/app/soapbox/features/group/components/__tests__/group-relationship.test.tsx
deleted file mode 100644
index 4c6c10a48..000000000
--- a/app/soapbox/features/group/components/__tests__/group-relationship.test.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-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 GroupRelationship from '../group-relationship';
-
-let group: Group;
-
-describe('', () => {
- describe('when the user is an admin', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: false,
- member: true,
- role: GroupRoles.ADMIN,
- }),
- });
- });
-
- it('should render the relationship', () => {
- render();
-
- expect(screen.getByTestId('group-relationship')).toHaveTextContent('Admin');
- });
- });
-
- describe('when the user is an owner', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: false,
- member: true,
- role: GroupRoles.OWNER,
- }),
- });
- });
-
- it('should render the relationship', () => {
- render();
-
- expect(screen.getByTestId('group-relationship')).toHaveTextContent('Owner');
- });
- });
-
- describe('when the user is a member', () => {
- beforeEach(() => {
- group = buildGroup({
- relationship: buildGroupRelationship({
- requested: false,
- member: true,
- role: GroupRoles.USER,
- }),
- });
- });
-
- it('should render null', () => {
- render();
-
- expect(screen.queryAllByTestId('group-relationship')).toHaveLength(0);
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx b/app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx
deleted file mode 100644
index f91853dc4..000000000
--- a/app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-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('', () => {
- describe('tag name', () => {
- const name = 'hello';
-
- it('should render the tag name', () => {
- const group = buildGroup();
- const tag = buildGroupTag({ name });
- render();
-
- 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();
- 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();
- 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();
- 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();
- 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();
- 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 pinned', () => {
- const tag = buildGroupTag({ pinned: true, visible: true });
-
- it('does render the pin icon', () => {
- render();
- screen.debug();
- expect(screen.queryAllByTestId('pin-icon')).toHaveLength(1);
- });
- });
-
- describe('when the tag is not pinned', () => {
- const tag = buildGroupTag({ pinned: false, visible: true });
-
- it('does not render the pin icon', () => {
- render();
- expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0);
- });
- });
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-action-button.tsx b/app/soapbox/features/group/components/group-action-button.tsx
deleted file mode 100644
index f3b208574..000000000
--- a/app/soapbox/features/group/components/group-action-button.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import React from 'react';
-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 { queryClient } from 'soapbox/queries/client';
-import { GroupKeys } from 'soapbox/queries/groups';
-import { GroupRoles } from 'soapbox/schemas/group-member';
-import toast from 'soapbox/toast';
-
-import type { Group, GroupRelationship } from 'soapbox/types/entities';
-
-interface IGroupActionButton {
- group: Group
-}
-
-const messages = defineMessages({
- confirmationConfirm: { id: 'confirmations.leave_group.confirm', defaultMessage: 'Leave' },
- confirmationHeading: { id: 'confirmations.leave_group.heading', defaultMessage: 'Leave group' },
- confirmationMessage: { id: 'confirmations.leave_group.message', defaultMessage: 'You are about to leave the group. Do you want to continue?' },
- joinRequestSuccess: { id: 'group.join.request_success', defaultMessage: 'Request sent to group owner' },
- joinSuccess: { id: 'group.join.success', defaultMessage: 'Group joined successfully!' },
- leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' },
-});
-
-const GroupActionButton = ({ group }: IGroupActionButton) => {
- const dispatch = useAppDispatch();
- const intl = useIntl();
- const account = useOwnAccount();
-
- const joinGroup = useJoinGroup(group);
- const leaveGroup = useLeaveGroup(group);
- const cancelRequest = useCancelMembershipRequest(group);
-
- const isRequested = group.relationship?.requested;
- const isNonMember = !group.relationship?.member && !isRequested;
- const isOwner = group.relationship?.role === GroupRoles.OWNER;
- const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
- const isBlocked = group.relationship?.blocked_by;
-
- const onJoinGroup = () => joinGroup.mutate({}, {
- onSuccess(entity) {
- joinGroup.invalidate();
- dispatch(fetchGroupRelationshipsSuccess([entity]));
- queryClient.invalidateQueries(GroupKeys.pendingGroups(account?.id as string));
-
- toast.success(
- group.locked
- ? intl.formatMessage(messages.joinRequestSuccess)
- : intl.formatMessage(messages.joinSuccess),
- );
- },
- onError(error) {
- const message = (error.response?.data as any).error;
- if (message) {
- toast.error(message);
- }
- },
- });
-
- const onLeaveGroup = () =>
- dispatch(openModal('CONFIRM', {
- heading: intl.formatMessage(messages.confirmationHeading),
- message: intl.formatMessage(messages.confirmationMessage),
- confirm: intl.formatMessage(messages.confirmationConfirm),
- onConfirm: () => leaveGroup.mutate(group.relationship?.id as string, {
- onSuccess(entity) {
- leaveGroup.invalidate();
- dispatch(fetchGroupRelationshipsSuccess([entity]));
- toast.success(intl.formatMessage(messages.leaveSuccess));
- },
- }),
- }));
-
- const onCancelRequest = () => cancelRequest.mutate({}, {
- onSuccess() {
- const entity = {
- ...group.relationship as GroupRelationship,
- requested: false,
- };
- dispatch(importEntities([entity], Entities.GROUP_RELATIONSHIPS));
- queryClient.invalidateQueries(GroupKeys.pendingGroups(account?.id as string));
- },
- });
-
- if (isBlocked) {
- return null;
- }
-
- if (isOwner || isAdmin) {
- return (
-
- );
- }
-
- if (isNonMember) {
- return (
-
- );
- }
-
- if (isRequested) {
- return (
-
- );
- }
-
- return (
-
- );
-};
-
-export default GroupActionButton;
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-avatar-picker.tsx b/app/soapbox/features/group/components/group-avatar-picker.tsx
deleted file mode 100644
index b13dfe80e..000000000
--- a/app/soapbox/features/group/components/group-avatar-picker.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import clsx from 'clsx';
-import React from 'react';
-
-import Icon from 'soapbox/components/icon';
-import { Avatar, HStack } from 'soapbox/components/ui';
-
-interface IMediaInput {
- src: string | undefined
- accept: string
- onChange: React.ChangeEventHandler
- disabled?: boolean
-}
-
-const AvatarPicker = React.forwardRef(({ src, onChange, accept, disabled }, ref) => {
- return (
-
- );
-});
-
-export default AvatarPicker;
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-header-image.tsx b/app/soapbox/features/group/components/group-header-image.tsx
deleted file mode 100644
index f40749536..000000000
--- a/app/soapbox/features/group/components/group-header-image.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import clsx from 'clsx';
-import React, { useState } from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-
-import { Icon } from 'soapbox/components/ui';
-
-import type { Group } from 'soapbox/schemas';
-
-const messages = defineMessages({
- header: { id: 'group.header.alt', defaultMessage: 'Group header' },
-});
-
-interface IGroupHeaderImage {
- group?: Group | false | null
- className?: string
-}
-
-const GroupHeaderImage: React.FC = ({ className, group }) => {
- const intl = useIntl();
-
- const [isHeaderMissing, setIsHeaderMissing] = useState(false);
-
- if (!group || !group.header) {
- return null;
- }
-
- if (isHeaderMissing) {
- return (
-
-
-
- );
- }
-
- return (
- setIsHeaderMissing(true)}
- />
- );
-};
-
-export default GroupHeaderImage;
diff --git a/app/soapbox/features/group/components/group-header-picker.tsx b/app/soapbox/features/group/components/group-header-picker.tsx
deleted file mode 100644
index d2457ac1e..000000000
--- a/app/soapbox/features/group/components/group-header-picker.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import clsx from 'clsx';
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-
-import Icon from 'soapbox/components/icon';
-import { HStack, Text } from 'soapbox/components/ui';
-
-interface IMediaInput {
- src: string | undefined
- accept: string
- onChange: React.ChangeEventHandler
- disabled?: boolean
-}
-
-const HeaderPicker = React.forwardRef(({ src, onChange, accept, disabled }, ref) => {
- return (
-
- );
-});
-
-export default HeaderPicker;
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-header.tsx b/app/soapbox/features/group/components/group-header.tsx
deleted file mode 100644
index 2491a7bb7..000000000
--- a/app/soapbox/features/group/components/group-header.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-import { List as ImmutableList } from 'immutable';
-import React, { useState } from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-
-import { openModal } from 'soapbox/actions/modals';
-import GroupAvatar from 'soapbox/components/groups/group-avatar';
-import StillImage from 'soapbox/components/still-image';
-import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
-import { useAppDispatch } from 'soapbox/hooks';
-import { normalizeAttachment } from 'soapbox/normalizers';
-import { isDefaultHeader } from 'soapbox/utils/accounts';
-
-import GroupActionButton from './group-action-button';
-import GroupMemberCount from './group-member-count';
-import GroupOptionsButton from './group-options-button';
-import GroupPrivacy from './group-privacy';
-import GroupRelationship from './group-relationship';
-
-import type { Group } from 'soapbox/types/entities';
-
-const messages = defineMessages({
- header: { id: 'group.header.alt', defaultMessage: 'Group header' },
-});
-
-interface IGroupHeader {
- group?: Group | false | null
-}
-
-const GroupHeader: React.FC = ({ group }) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const [isHeaderMissing, setIsHeaderMissing] = useState(false);
-
- if (!group) {
- return (
-
- );
- }
-
- const isDeleted = !!group.deleted_at;
-
- const onAvatarClick = () => {
- const avatar = normalizeAttachment({
- type: 'image',
- url: group.avatar,
- });
- dispatch(openModal('MEDIA', { media: ImmutableList.of(avatar), index: 0 }));
- };
-
- const handleAvatarClick: React.MouseEventHandler = (e) => {
- if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- onAvatarClick();
- }
- };
-
- const onHeaderClick = () => {
- const header = normalizeAttachment({
- type: 'image',
- url: group.header,
- });
- dispatch(openModal('MEDIA', { media: ImmutableList.of(header), index: 0 }));
- };
-
- const handleHeaderClick: React.MouseEventHandler = (e) => {
- if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- onHeaderClick();
- }
- };
-
- const renderHeader = () => {
- let header: React.ReactNode;
-
- if (group.header) {
- header = (
- setIsHeaderMissing(true)}
- />
- );
-
- if (!isDefaultHeader(group.header)) {
- header = (
-
- {header}
-
- );
- }
- }
-
- return (
-
- {isHeaderMissing ? (
-
- ) : header}
-
- );
- };
-
- return (
-
-
-
-
-
-
- {!isDeleted && (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
-
-
- );
-};
-
-export default GroupHeader;
diff --git a/app/soapbox/features/group/components/group-member-count.tsx b/app/soapbox/features/group/components/group-member-count.tsx
deleted file mode 100644
index d6e0223f4..000000000
--- a/app/soapbox/features/group/components/group-member-count.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router-dom';
-
-import { Text } from 'soapbox/components/ui';
-import { Group } from 'soapbox/types/entities';
-import { shortNumberFormat } from 'soapbox/utils/numbers';
-
-interface IGroupMemberCount {
- group: Group
-}
-
-const GroupMemberCount = ({ group }: IGroupMemberCount) => {
- return (
-
-
- {shortNumberFormat(group.members_count)}
- {' '}
-
-
-
- );
-};
-
-export default GroupMemberCount;
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-member-list-item.tsx b/app/soapbox/features/group/components/group-member-list-item.tsx
deleted file mode 100644
index a66a2db6e..000000000
--- a/app/soapbox/features/group/components/group-member-list-item.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-import clsx from 'clsx';
-import React, { useMemo } from 'react';
-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';
-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 { GroupRoles } from 'soapbox/schemas/group-member';
-import toast from 'soapbox/toast';
-
-import { MAX_ADMIN_COUNT } from '../group-members';
-
-import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
-import type { Group, GroupMember } from 'soapbox/types/entities';
-
-const messages = defineMessages({
- adminLimitTitle: { id: 'group.member.admin.limit.title', defaultMessage: 'Admin limit reached' },
- adminLimitSummary: { id: 'group.member.admin.limit.summary', defaultMessage: 'You can assign up to {count} admins for the group at this time.' },
- blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Ban' },
- blockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Ban From Group' },
- blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to ban @{name} from the group?' },
- blocked: { id: 'group.group_mod_block.success', defaultMessage: '@{name} is banned' },
- demotedToUser: { id: 'group.demote.user.success', defaultMessage: '@{name} is now a member' },
- groupModBlock: { id: 'group.group_mod_block', defaultMessage: 'Ban from group' },
- groupModDemote: { id: 'group.group_mod_demote', defaultMessage: 'Remove {role} role' },
- groupModKick: { id: 'group.group_mod_kick', defaultMessage: 'Kick @{name} from group' },
- groupModPromoteMod: { id: 'group.group_mod_promote_mod', defaultMessage: 'Assign {role} role' },
- kickConfirm: { id: 'confirmations.kick_from_group.confirm', defaultMessage: 'Kick' },
- kickFromGroupMessage: { id: 'confirmations.kick_from_group.message', defaultMessage: 'Are you sure you want to kick @{name} from this group?' },
- kicked: { id: 'group.group_mod_kick.success', defaultMessage: 'Kicked @{name} from group' },
- promoteConfirm: { id: 'group.promote.admin.confirmation.title', defaultMessage: 'Assign Admin Role' },
- promoteConfirmMessage: { id: 'group.promote.admin.confirmation.message', defaultMessage: 'Are you sure you want to assign the admin role to @{name}?' },
- promotedToAdmin: { id: 'group.promote.admin.success', defaultMessage: '@{name} is now an admin' },
-});
-
-interface IGroupMemberListItem {
- member: GroupMember
- group: Group
- canPromoteToAdmin: boolean
-}
-
-const GroupMemberListItem = (props: IGroupMemberListItem) => {
- const { canPromoteToAdmin, member, group } = props;
-
- const dispatch = useAppDispatch();
- const features = useFeatures();
- const intl = useIntl();
-
- const blockGroupMember = useBlockGroupMember(group, member);
- const promoteGroupMember = usePromoteGroupMember(group, member);
- const demoteGroupMember = useDemoteGroupMember(group, member);
-
- const { account, isLoading } = useAccount(member.account.id);
-
- // Current user role
- const isCurrentUserOwner = group.relationship?.role === GroupRoles.OWNER;
- const isCurrentUserAdmin = group.relationship?.role === GroupRoles.ADMIN;
-
- // Member role
- const isMemberOwner = member.role === GroupRoles.OWNER;
- const isMemberAdmin = member.role === GroupRoles.ADMIN;
- const isMemberUser = member.role === GroupRoles.USER;
-
- const handleKickFromGroup = () => {
- dispatch(openModal('CONFIRM', {
- message: intl.formatMessage(messages.kickFromGroupMessage, { name: account?.username }),
- confirm: intl.formatMessage(messages.kickConfirm),
- onConfirm: () => dispatch(groupKick(group.id, account?.id as string)).then(() =>
- toast.success(intl.formatMessage(messages.kicked, { name: account?.acct })),
- ),
- }));
- };
-
- const handleBlockFromGroup = () => {
- dispatch(openModal('CONFIRM', {
- heading: intl.formatMessage(messages.blockFromGroupHeading),
- message: intl.formatMessage(messages.blockFromGroupMessage, { name: account?.username }),
- confirm: intl.formatMessage(messages.blockConfirm),
- onConfirm: () => {
- blockGroupMember({ account_ids: [member.account.id] }, {
- onSuccess() {
- dispatch(deleteEntities([member.id], Entities.GROUP_MEMBERSHIPS));
- toast.success(intl.formatMessage(messages.blocked, { name: account?.acct }));
- },
- });
- },
- }));
- };
-
- const handleAdminAssignment = () => {
- if (!canPromoteToAdmin) {
- toast.error(intl.formatMessage(messages.adminLimitTitle), {
- summary: intl.formatMessage(messages.adminLimitSummary, { count: MAX_ADMIN_COUNT }),
- });
- return;
- }
-
- dispatch(openModal('CONFIRM', {
- heading: intl.formatMessage(messages.promoteConfirm),
- message: intl.formatMessage(messages.promoteConfirmMessage, { name: account?.username }),
- confirm: intl.formatMessage(messages.promoteConfirm),
- confirmationTheme: 'primary',
- onConfirm: () => {
- promoteGroupMember({ role: GroupRoles.ADMIN, account_ids: [account?.id] }, {
- onSuccess() {
- toast.success(
- intl.formatMessage(messages.promotedToAdmin, { name: account?.acct }),
- );
- },
- });
- },
- }));
- };
-
- const handleUserAssignment = () => {
- demoteGroupMember({ role: GroupRoles.USER, account_ids: [account?.id] }, {
- onSuccess() {
- toast.success(intl.formatMessage(messages.demotedToUser, { name: account?.acct }));
- },
- });
- };
-
- const menu: IMenu = useMemo(() => {
- const items: IMenu = [];
-
- if (!group || !account || !group.relationship?.role) {
- return items;
- }
-
- if (isCurrentUserOwner) {
- if (isMemberUser) {
- items.push({
- text: intl.formatMessage(messages.groupModPromoteMod, { role: GroupRoles.ADMIN }),
- icon: require('@tabler/icons/briefcase.svg'),
- action: handleAdminAssignment,
- });
- } else if (isMemberAdmin) {
- items.push({
- text: intl.formatMessage(messages.groupModDemote, { role: GroupRoles.ADMIN, name: account.username }),
- icon: require('@tabler/icons/briefcase.svg'),
- action: handleUserAssignment,
- destructive: true,
- });
- }
- }
-
- if (
- (isCurrentUserOwner || isCurrentUserAdmin) &&
- (isMemberAdmin || isMemberUser) &&
- member.role !== group.relationship.role
- ) {
- if (features.groupsKick) {
- items.push({
- text: intl.formatMessage(messages.groupModKick, { name: account.username }),
- icon: require('@tabler/icons/user-minus.svg'),
- action: handleKickFromGroup,
- });
- }
-
- items.push({
- text: intl.formatMessage(messages.groupModBlock, { name: account.username }),
- icon: require('@tabler/icons/ban.svg'),
- action: handleBlockFromGroup,
- destructive: true,
- });
- }
-
- return items;
- }, [group, account?.id]);
-
- if (isLoading) {
- return ;
- }
-
- return (
-
-
-
-
- {(isMemberOwner || isMemberAdmin) ? (
-
- {member.role}
-
- ) : null}
-
-
-
-
- );
-};
-
-export default GroupMemberListItem;
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-options-button.tsx b/app/soapbox/features/group/components/group-options-button.tsx
deleted file mode 100644
index ebc3152e4..000000000
--- a/app/soapbox/features/group/components/group-options-button.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React, { useMemo } from 'react';
-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 { GroupRoles } from 'soapbox/schemas/group-member';
-import toast from 'soapbox/toast';
-
-import type { Account, Group } from 'soapbox/types/entities';
-
-const messages = defineMessages({
- confirmationConfirm: { id: 'confirmations.leave_group.confirm', defaultMessage: 'Leave' },
- confirmationHeading: { id: 'confirmations.leave_group.heading', defaultMessage: 'Leave group' },
- confirmationMessage: { id: 'confirmations.leave_group.message', defaultMessage: 'You are about to leave the group. Do you want to continue?' },
- leave: { id: 'group.leave.label', defaultMessage: 'Leave' },
- leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' },
- report: { id: 'group.report.label', defaultMessage: 'Report' },
- share: { id: 'group.share.label', defaultMessage: 'Share' },
-});
-
-interface IGroupActionButton {
- group: Group
-}
-
-const GroupOptionsButton = ({ group }: IGroupActionButton) => {
- const account = useOwnAccount();
- const dispatch = useAppDispatch();
- const intl = useIntl();
- const leaveGroup = useLeaveGroup(group);
-
- const isMember = group.relationship?.role === GroupRoles.USER;
- const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
- const isBlocked = group.relationship?.blocked_by;
-
- const handleShare = () => {
- navigator.share({
- text: group.display_name,
- url: group.url,
- }).catch((e) => {
- if (e.name !== 'AbortError') console.error(e);
- });
- };
-
- const onLeaveGroup = () =>
- dispatch(openModal('CONFIRM', {
- heading: intl.formatMessage(messages.confirmationHeading),
- message: intl.formatMessage(messages.confirmationMessage),
- confirm: intl.formatMessage(messages.confirmationConfirm),
- onConfirm: () => leaveGroup.mutate(group.relationship?.id as string, {
- onSuccess() {
- leaveGroup.invalidate();
- toast.success(intl.formatMessage(messages.leaveSuccess));
- },
- }),
- }));
-
- const menu: Menu = useMemo(() => {
- const canShare = 'share' in navigator;
- const items = [];
-
- if (isMember || isAdmin) {
- items.push({
- text: intl.formatMessage(messages.report),
- icon: require('@tabler/icons/flag.svg'),
- action: () => dispatch(initReport(ReportableEntities.GROUP, account as Account, { group })),
- });
- }
-
- if (canShare) {
- items.push({
- text: intl.formatMessage(messages.share),
- icon: require('@tabler/icons/share.svg'),
- action: handleShare,
- });
- }
-
- if (isAdmin) {
- items.push({
- text: intl.formatMessage(messages.leave),
- icon: require('@tabler/icons/logout.svg'),
- action: onLeaveGroup,
- });
- }
-
- return items;
- }, [isMember, isAdmin]);
-
- if (isBlocked || menu.length === 0) {
- return null;
- }
-
- return (
-
-
-
- );
-};
-
-export default GroupOptionsButton;
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-privacy.tsx b/app/soapbox/features/group/components/group-privacy.tsx
deleted file mode 100644
index 952c8bd49..000000000
--- a/app/soapbox/features/group/components/group-privacy.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-
-import { HStack, Icon, Popover, Stack, Text } from 'soapbox/components/ui';
-import { Group } from 'soapbox/types/entities';
-
-interface IGroupPolicy {
- group: Group
-}
-
-const GroupPrivacy = ({ group }: IGroupPolicy) => (
-
-
-
-
-
-
-
- {group.locked ? (
-
- ) : (
-
- )}
-
-
-
- {group.locked ? (
-
- ) : (
-
- )}
-
-
-
- }
- >
-
-
-
-
- {group.locked ? (
-
- ) : (
-
- )}
-
-
-
-);
-
-export default GroupPrivacy;
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-relationship.tsx b/app/soapbox/features/group/components/group-relationship.tsx
deleted file mode 100644
index 8fd47ac2d..000000000
--- a/app/soapbox/features/group/components/group-relationship.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-
-import { HStack, Icon, Text } from 'soapbox/components/ui';
-import { GroupRoles } from 'soapbox/schemas/group-member';
-import { Group } from 'soapbox/types/entities';
-
-interface IGroupRelationship {
- group: Group
-}
-
-const GroupRelationship = ({ group }: IGroupRelationship) => {
- const isOwner = group.relationship?.role === GroupRoles.OWNER;
- const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
-
- if (!isOwner && !isAdmin) {
- return null;
- }
-
- return (
-
-
-
-
- {isOwner
- ?
- : }
-
-
- );
-};
-
-export default GroupRelationship;
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-tag-list-item.tsx b/app/soapbox/features/group/components/group-tag-list-item.tsx
deleted file mode 100644
index 07660cf21..000000000
--- a/app/soapbox/features/group/components/group-tag-list-item.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-import React from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-import { Link } from 'react-router-dom';
-
-import { useUpdateGroupTag } from 'soapbox/api/hooks';
-import { HStack, Icon, 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 { GroupRoles } from 'soapbox/schemas/group-member';
-import toast from 'soapbox/toast';
-import { shortNumberFormat } from 'soapbox/utils/numbers';
-
-import type { Group, GroupTag } from 'soapbox/schemas';
-
-const messages = defineMessages({
- hideTag: { id: 'group.tags.hide', defaultMessage: 'Hide topic' },
- showTag: { id: 'group.tags.show', defaultMessage: 'Show topic' },
- total: { id: 'group.tags.total', defaultMessage: 'Total Posts' },
- pinTag: { id: 'group.tags.pin', defaultMessage: 'Pin topic' },
- unpinTag: { id: 'group.tags.unpin', defaultMessage: 'Unpin topic' },
- pinSuccess: { id: 'group.tags.pin.success', defaultMessage: 'Pinned!' },
- unpinSuccess: { id: 'group.tags.unpin.success', defaultMessage: 'Unpinned!' },
- visibleSuccess: { id: 'group.tags.visible.success', defaultMessage: 'Topic marked as visible' },
- hiddenSuccess: { id: 'group.tags.hidden.success', defaultMessage: 'Topic marked as hidden' },
-});
-
-interface IGroupMemberListItem {
- tag: GroupTag
- group: Group
- isPinnable: boolean
-}
-
-const GroupTagListItem = (props: IGroupMemberListItem) => {
- const { group, tag, isPinnable } = props;
- const dispatch = useAppDispatch();
-
- const intl = useIntl();
- const { updateGroupTag } = useUpdateGroupTag(group.id, tag.id);
-
- const isOwner = group.relationship?.role === GroupRoles.OWNER;
-
- const toggleVisibility = () => {
- const isHiding = tag.visible;
-
- updateGroupTag({
- group_tag_type: isHiding ? 'hidden' : 'normal',
- }, {
- onSuccess() {
- const entity: GroupTag = {
- ...tag,
- visible: !tag.visible,
- pinned: isHiding ? false : tag.pinned, // unpin if we're hiding
- };
- dispatch(importEntities([entity], Entities.GROUP_TAGS));
-
- toast.success(
- entity.visible ?
- intl.formatMessage(messages.visibleSuccess) :
- intl.formatMessage(messages.hiddenSuccess),
- );
- },
- });
- };
-
- const togglePin = () => {
- updateGroupTag({
- group_tag_type: tag.pinned ? 'normal' : 'pinned',
- }, {
- onSuccess() {
- const entity = {
- ...tag,
- pinned: !tag.pinned,
- };
- dispatch(importEntities([entity], Entities.GROUP_TAGS));
-
- toast.success(
- entity.pinned ?
- intl.formatMessage(messages.pinSuccess) :
- intl.formatMessage(messages.unpinSuccess),
- );
- },
- });
- };
-
- const renderPinIcon = () => {
- if (!isOwner && tag.pinned) {
- return (
-
- );
- }
-
- if (!isOwner) {
- return null;
- }
-
- if (isPinnable) {
- return (
-
-
-
- );
- }
-
- if (!isPinnable && tag.pinned) {
- return (
-
-
-
-
- );
- }
- };
-
- return (
-
-
-
-
- #{tag.name}
-
-
- {intl.formatMessage(messages.total)}:
- {' '}
-
- {shortNumberFormat(tag.uses)}
-
-
-
-
-
-
- {tag.visible ? (
- renderPinIcon()
- ) : null}
-
- {isOwner ? (
-
-
-
- ) : null}
-
-
- );
-};
-
-export default GroupTagListItem;
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-tags-field.tsx b/app/soapbox/features/group/components/group-tags-field.tsx
deleted file mode 100644
index f8092d5c0..000000000
--- a/app/soapbox/features/group/components/group-tags-field.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React, { useMemo } from 'react';
-import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
-
-import { Input, Streamfield } from 'soapbox/components/ui';
-
-import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
-
-const messages = defineMessages({
- hashtagPlaceholder: { id: 'manage_group.fields.hashtag_placeholder', defaultMessage: 'Add a topic' },
-});
-
-interface IGroupTagsField {
- tags: string[]
- onChange(tags: string[]): void
- onAddItem(): void
- onRemoveItem(i: number): void
- maxItems?: number
-}
-
-const GroupTagsField: React.FC = ({ tags, onChange, onAddItem, onRemoveItem, maxItems = 3 }) => {
- return (
- }
- hint={}
- component={HashtagField}
- values={tags}
- onChange={onChange}
- onAddItem={onAddItem}
- onRemoveItem={onRemoveItem}
- maxItems={maxItems}
- minItems={1}
- />
- );
-};
-
-const HashtagField: StreamfieldComponent = ({ value, onChange, autoFocus = false }) => {
- const intl = useIntl();
-
- const formattedValue = useMemo(() => {
- return `#${value}`;
- }, [value]);
-
- const handleChange: React.ChangeEventHandler = ({ target }) => {
- onChange(target.value.replace('#', ''));
- };
-
- return (
-
- );
-};
-
-export default GroupTagsField;
\ No newline at end of file
diff --git a/app/soapbox/features/group/edit-group.tsx b/app/soapbox/features/group/edit-group.tsx
deleted file mode 100644
index 82f4841b4..000000000
--- a/app/soapbox/features/group/edit-group.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-
-import { useGroup, useGroupTags, 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 { useImageField, useTextField } from 'soapbox/hooks/forms';
-import toast from 'soapbox/toast';
-import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts';
-
-import AvatarPicker from './components/group-avatar-picker';
-import HeaderPicker from './components/group-header-picker';
-import GroupTagsField from './components/group-tags-field';
-
-import type { List as ImmutableList } from 'immutable';
-
-const nonDefaultAvatar = (url: string | undefined) => url && isDefaultAvatar(url) ? undefined : url;
-const nonDefaultHeader = (url: string | undefined) => url && isDefaultHeader(url) ? undefined : url;
-
-const messages = defineMessages({
- heading: { id: 'navigation_bar.edit_group', defaultMessage: 'Edit Group' },
- groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' },
- groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' },
- groupSaved: { id: 'group.update.success', defaultMessage: 'Group successfully saved' },
-});
-
-interface IEditGroup {
- params: {
- groupId: string
- }
-}
-
-const EditGroup: React.FC = ({ params: { groupId } }) => {
- const intl = useIntl();
- const instance = useInstance();
-
- const { group, isLoading } = useGroup(groupId);
- const { updateGroup } = useUpdateGroup(groupId);
- const { invalidate } = useGroupTags(groupId);
-
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [tags, setTags] = useState(['']);
-
- const avatar = useImageField({ maxPixels: 400 * 400, preview: nonDefaultAvatar(group?.avatar) });
- const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(group?.header) });
-
- const displayName = useTextField(group?.display_name);
- const note = useTextField(group?.note_plain);
-
- const maxName = Number(instance.configuration.getIn(['groups', 'max_characters_name']));
- const maxNote = Number(instance.configuration.getIn(['groups', 'max_characters_description']));
-
- const attachmentTypes = useAppSelector(
- state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList,
- )?.filter(type => type.startsWith('image/')).toArray().join(',');
-
- async function handleSubmit() {
- setIsSubmitting(true);
-
- await updateGroup({
- display_name: displayName.value,
- note: note.value,
- avatar: avatar.file,
- header: header.file,
- tags,
- }, {
- onSuccess() {
- invalidate();
- toast.success(intl.formatMessage(messages.groupSaved));
- },
- onError(error) {
- const message = (error.response?.data as any)?.error;
-
- if (error.response?.status === 422 && typeof message !== 'undefined') {
- toast.error(message);
- }
- },
- });
-
- setIsSubmitting(false);
- }
-
- const handleAddTag = () => {
- setTags([...tags, '']);
- };
-
- const handleRemoveTag = (i: number) => {
- const newTags = [...tags];
- newTags.splice(i, 1);
- setTags(newTags);
- };
-
- useEffect(() => {
- if (group) {
- setTags(group.tags.map((t) => t.name));
- }
- }, [group?.id]);
-
- if (isLoading) {
- return ;
- }
-
- return (
-
-
-
- );
-};
-
-export default EditGroup;
diff --git a/app/soapbox/features/group/group-blocked-members.tsx b/app/soapbox/features/group/group-blocked-members.tsx
deleted file mode 100644
index f45e8259f..000000000
--- a/app/soapbox/features/group/group-blocked-members.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-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 { makeGetAccount } from 'soapbox/selectors';
-import toast from 'soapbox/toast';
-
-import ColumnForbidden from '../ui/components/column-forbidden';
-
-type RouteParams = { groupId: string };
-
-const messages = defineMessages({
- heading: { id: 'column.group_blocked_members', defaultMessage: 'Banned Members' },
- unblock: { id: 'group.group_mod_unblock', defaultMessage: 'Unban' },
- unblocked: { id: 'group.group_mod_unblock.success', defaultMessage: 'Unbanned @{name} from group' },
-});
-
-interface IBlockedMember {
- accountId: string
- groupId: string
-}
-
-const BlockedMember: React.FC = ({ accountId, groupId }) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const getAccount = useCallback(makeGetAccount(), []);
-
- const account = useAppSelector((state) => getAccount(state, accountId));
-
- if (!account) return null;
-
- const handleUnblock = () =>
- dispatch(groupUnblock(groupId, accountId))
- .then(() => toast.success(intl.formatMessage(messages.unblocked, { name: account.acct })));
-
- return (
-
-
-
-
-
- );
-};
-
-interface IGroupBlockedMembers {
- params: RouteParams
-}
-
-const GroupBlockedMembers: React.FC = ({ params }) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const id = params?.groupId;
-
- const { group } = useGroup(id);
- const accountIds = useAppSelector((state) => state.user_lists.group_blocks.get(id)?.items);
-
- useEffect(() => {
- dispatch(fetchGroupBlocks(id));
- }, [id]);
-
- if (!group || !group.relationship || !accountIds) {
- return (
-
-
-
- );
- }
-
- if (!group.relationship.role || !['owner', 'admin', 'moderator'].includes(group.relationship.role)) {
- return ();
- }
-
- const emptyMessage = ;
-
- return (
-
-
- {accountIds.map((accountId) =>
- ,
- )}
-
-
- );
-};
-
-export default GroupBlockedMembers;
diff --git a/app/soapbox/features/group/group-gallery.tsx b/app/soapbox/features/group/group-gallery.tsx
deleted file mode 100644
index f0b2b95c7..000000000
--- a/app/soapbox/features/group/group-gallery.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-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 MediaItem from '../account-gallery/components/media-item';
-
-import type { Attachment, Status } from 'soapbox/types/entities';
-
-interface IGroupGallery {
- params: { groupId: string }
-}
-
-const GroupGallery: React.FC = (props) => {
- const { groupId } = props.params;
-
- const dispatch = useAppDispatch();
-
- const { group, isLoading: groupIsLoading } = useGroup(groupId);
-
- const {
- entities: statuses,
- fetchNextPage,
- isLoading,
- isFetching,
- hasNextPage,
- } = useGroupMedia(groupId);
-
- const attachments = statuses.reduce((result, status) => {
- result.push(...status.media_attachments.map((a) => a.set('status', status)));
- return result;
- }, []);
-
- const handleOpenMedia = (attachment: Attachment) => {
- if (attachment.type === 'video') {
- dispatch(openModal('VIDEO', { media: attachment, status: attachment.status, account: attachment.account }));
- } else {
- const media = (attachment.status as Status).media_attachments;
- const index = media.findIndex((x) => x.id === attachment.id);
-
- dispatch(openModal('MEDIA', { media, index, status: attachment.status }));
- }
- };
-
- if (isLoading || groupIsLoading) {
- return (
-
-
-
-
-
- );
- }
-
- if (!group) {
- return (
-
-
-
- );
- }
-
- return (
-
-
- {attachments.map((attachment) => (
-
- ))}
-
- {(!isLoading && attachments.length === 0) && (
-
-
-
- )}
-
-
- {hasNextPage && (
-
- )}
-
- );
-};
-
-export default GroupGallery;
diff --git a/app/soapbox/features/group/group-members.tsx b/app/soapbox/features/group/group-members.tsx
deleted file mode 100644
index 1c2a892f3..000000000
--- a/app/soapbox/features/group/group-members.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-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 { GroupRoles } from 'soapbox/schemas/group-member';
-
-import PlaceholderAccount from '../placeholder/components/placeholder-account';
-
-import GroupMemberListItem from './components/group-member-list-item';
-
-import type { Group } from 'soapbox/types/entities';
-
-
-interface IGroupMembers {
- params: { groupId: string }
-}
-
-export const MAX_ADMIN_COUNT = 5;
-
-const GroupMembers: React.FC = (props) => {
- const { groupId } = props.params;
-
- const features = useFeatures();
-
- const { group, isFetching: isFetchingGroup } = useGroup(groupId);
- const { groupMembers: owners, isFetching: isFetchingOwners } = useGroupMembers(groupId, GroupRoles.OWNER);
- const { groupMembers: admins, isFetching: isFetchingAdmins } = useGroupMembers(groupId, GroupRoles.ADMIN);
- const { groupMembers: users, isFetching: isFetchingUsers, fetchNextPage, hasNextPage } = useGroupMembers(groupId, GroupRoles.USER);
- const { isFetching: isFetchingPending, count: pendingCount } = useGroupMembershipRequests(groupId);
-
- const isLoading = isFetchingGroup || isFetchingOwners || isFetchingAdmins || isFetchingUsers || isFetchingPending;
-
- const members = useMemo(() => [
- ...owners,
- ...admins,
- ...users,
- ], [owners, admins, users]);
-
- const canPromoteToAdmin = features.groupsAdminMax
- ? members.filter((member) => member.role === GroupRoles.ADMIN).length < MAX_ADMIN_COUNT
- : true;
-
- return (
- <>
- 0) && (
-
- )}
- >
- {members.map((member) => (
-
- ))}
-
- >
- );
-};
-
-export default GroupMembers;
diff --git a/app/soapbox/features/group/group-membership-requests.tsx b/app/soapbox/features/group/group-membership-requests.tsx
deleted file mode 100644
index 79700e1a7..000000000
--- a/app/soapbox/features/group/group-membership-requests.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-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 { GroupRoles } from 'soapbox/schemas/group-member';
-import toast from 'soapbox/toast';
-
-import ColumnForbidden from '../ui/components/column-forbidden';
-
-import type { Account as AccountEntity } from 'soapbox/schemas';
-
-type RouteParams = { groupId: string };
-
-const messages = defineMessages({
- heading: { id: 'column.group_pending_requests', defaultMessage: 'Pending requests' },
- authorizeFail: { id: 'group.group_mod_authorize.fail', defaultMessage: 'Failed to approve @{name}' },
- rejectFail: { id: 'group.group_mod_reject.fail', defaultMessage: 'Failed to reject @{name}' },
-});
-
-interface IMembershipRequest {
- account: AccountEntity
- onAuthorize(account: AccountEntity): Promise
- onReject(account: AccountEntity): Promise
-}
-
-const MembershipRequest: React.FC = ({ account, onAuthorize, onReject }) => {
- if (!account) return null;
-
- const handleAuthorize = () => onAuthorize(account);
- const handleReject = () => onReject(account);
-
- return (
-
-
-
-
-
- );
-};
-
-interface IGroupMembershipRequests {
- params: RouteParams
-}
-
-const GroupMembershipRequests: React.FC = ({ params }) => {
- const id = params?.groupId;
- const intl = useIntl();
-
- const { group } = useGroup(id);
-
- const { accounts, authorize, reject, refetch, isLoading } = useGroupMembershipRequests(id);
- const { invalidate } = useGroupMembers(id, GroupRoles.USER);
-
- useEffect(() => {
- return () => {
- invalidate();
- };
- }, []);
-
- if (!group || !group.relationship || isLoading) {
- return (
-
-
-
- );
- }
-
- if (!group.relationship.role || !['owner', 'admin', 'moderator'].includes(group.relationship.role)) {
- return ;
- }
-
- async function handleAuthorize(account: AccountEntity) {
- 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) {
- 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 (
-
- }
- >
- {accounts.map((account) => (
-
- ))}
-
-
- );
-};
-
-export default GroupMembershipRequests;
diff --git a/app/soapbox/features/group/group-tag-timeline.tsx b/app/soapbox/features/group/group-tag-timeline.tsx
deleted file mode 100644
index e79092e7a..000000000
--- a/app/soapbox/features/group/group-tag-timeline.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-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 Timeline from '../ui/components/timeline';
-
-type RouteParams = { tagId: string, groupId: string };
-
-interface IGroupTimeline {
- params: RouteParams
-}
-
-const GroupTagTimeline: React.FC = (props) => {
- const dispatch = useAppDispatch();
- const groupId = props.params.groupId;
- const tagId = props.params.tagId;
-
- const { group } = useGroup(groupId);
- const { tag, isLoading } = useGroupTag(tagId);
-
- const handleLoadMore = (maxId: string) => {
- dispatch(expandGroupTimelineFromTag(groupId, tag?.name as string, { maxId }));
- };
-
- useEffect(() => {
- if (tag?.name) {
- dispatch(expandGroupTimelineFromTag(groupId, tag?.name));
- }
- }, [groupId, tag]);
-
-
- if (isLoading || !tag || !group) {
- return null;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
- }
- />
-
- );
-};
-
-export default GroupTagTimeline;
diff --git a/app/soapbox/features/group/group-tags.tsx b/app/soapbox/features/group/group-tags.tsx
deleted file mode 100644
index d5335e844..000000000
--- a/app/soapbox/features/group/group-tags.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-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 { useGroup } from 'soapbox/queries/groups';
-
-import PlaceholderAccount from '../placeholder/components/placeholder-account';
-
-import GroupTagListItem from './components/group-tag-list-item';
-
-import type { Group } from 'soapbox/types/entities';
-
-interface IGroupTopics {
- params: { groupId: string }
-}
-
-const GroupTopics: React.FC = (props) => {
- const { groupId } = props.params;
-
- const { group, isFetching: isFetchingGroup } = useGroup(groupId);
- const { tags, isFetching: isFetchingTags, hasNextPage, fetchNextPage } = useGroupTags(groupId);
-
- const isLoading = isFetchingGroup || isFetchingTags;
-
- const pinnedTags = tags.filter((tag) => tag.pinned);
- const isPinnable = pinnedTags.length < 3;
-
- return (
-
-
-
-
-
-
-
-
-
- }
- emptyMessageCard={false}
- >
- {tags.map((tag) => (
-
- ))}
-
- );
-};
-
-export default GroupTopics;
diff --git a/app/soapbox/features/group/group-timeline.tsx b/app/soapbox/features/group/group-timeline.tsx
deleted file mode 100644
index a920a862c..000000000
--- a/app/soapbox/features/group/group-timeline.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import clsx from 'clsx';
-import React, { useEffect, useRef } from 'react';
-import { FormattedMessage, useIntl } from 'react-intl';
-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 Timeline from '../ui/components/timeline';
-
-type RouteParams = { groupId: string };
-
-interface IGroupTimeline {
- params: RouteParams
-}
-
-const GroupTimeline: React.FC = (props) => {
- const intl = useIntl();
- const account = useOwnAccount();
- const dispatch = useAppDispatch();
- const composer = useRef(null);
-
- const { groupId } = props.params;
-
- const { group } = useGroup(groupId);
-
- const composeId = `group:${groupId}`;
- const canComposeGroupStatus = !!account && group?.relationship?.member;
- const groupTimelineVisible = useAppSelector((state) => !!state.compose.get(composeId)?.group_timeline_visible);
-
- const { isDragging, isDraggedOver } = useDraggedFiles(composer, (files) => {
- dispatch(uploadCompose(composeId, files, intl));
- });
-
- const handleLoadMore = (maxId: string) => {
- dispatch(expandGroupTimeline(groupId, { maxId }));
- };
-
- const handleToggleChange = () => {
- dispatch(setGroupTimelineVisible(composeId, !groupTimelineVisible));
- };
-
- useEffect(() => {
- dispatch(expandGroupTimeline(groupId));
- dispatch(groupCompose(composeId, groupId));
-
- const disconnect = dispatch(connectGroupStream(groupId));
-
- return () => {
- disconnect();
- };
- }, [groupId]);
-
- if (!group) {
- return null;
- }
-
- return (
-
- {canComposeGroupStatus && (
-
-
-
-
-
-
-
-
-
-
- )}
- />
-
-
- )}
-
-
-
-
-
-
-
-
-
-
- }
- emptyMessageCard={false}
- divideType='border'
- showGroup={false}
- />
-
- );
-};
-
-export default GroupTimeline;
diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx
deleted file mode 100644
index d57b8792a..000000000
--- a/app/soapbox/features/group/manage-group.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import React from 'react';
-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 { GroupRoles } from 'soapbox/schemas/group-member';
-import toast from 'soapbox/toast';
-import { TRUTHSOCIAL } from 'soapbox/utils/features';
-
-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' },
- 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' },
- deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' },
- 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' },
- deleteSuccess: { id: 'group.delete.success', defaultMessage: 'Group successfully deleted' },
-});
-
-interface IManageGroup {
- params: RouteParams
-}
-
-const ManageGroup: React.FC = ({ params }) => {
- const { groupId: id } = params;
-
- const backend = useBackend();
- const dispatch = useAppDispatch();
- const groupsPath = useGroupsPath();
- const history = useHistory();
- const intl = useIntl();
-
- const { group } = useGroup(id);
-
- const deleteGroup = useDeleteGroup();
-
- const isOwner = group?.relationship?.role === GroupRoles.OWNER;
-
- if (!group || !group.relationship) {
- return (
-
-
-
- );
- }
-
- if (!group.relationship.role || !['owner', 'admin', 'moderator'].includes(group.relationship.role)) {
- return ();
- }
-
- const onDeleteGroup = () =>
- dispatch(openModal('CONFIRM', {
- icon: require('@tabler/icons/trash.svg'),
- heading: intl.formatMessage(messages.deleteHeading),
- message: intl.formatMessage(messages.deleteMessage),
- confirm: intl.formatMessage(messages.deleteConfirm),
- onConfirm: () => {
- deleteGroup.mutate(group.id, {
- onSuccess() {
- toast.success(intl.formatMessage(messages.deleteSuccess));
- history.push(groupsPath);
- },
- });
- },
- }));
-
- const navigateToEdit = () => history.push(`/group/${group.slug}/manage/edit`);
- const navigateToPending = () => history.push(`/group/${group.slug}/manage/requests`);
- const navigateToBlocks = () => history.push(`/group/${group.slug}/manage/blocks`);
-
- return (
-
-
- {isOwner && (
- <>
-
-
-
-
-
-
-
-
-
- >
- )}
-
-
-
-
-
-
- {backend.software !== TRUTHSOCIAL && (
-
- )}
-
-
-
-
- {isOwner && (
- <>
-
-
-
-
-
- {intl.formatMessage(messages.deleteGroup)}} onClick={onDeleteGroup} />
-
- >
- )}
-
-
- );
-};
-
-export default ManageGroup;
diff --git a/app/soapbox/features/groups/__tests__/discover.test.tsx b/app/soapbox/features/groups/__tests__/discover.test.tsx
deleted file mode 100644
index b2485cdde..000000000
--- a/app/soapbox/features/groups/__tests__/discover.test.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import userEvent from '@testing-library/user-event';
-import { Map as ImmutableMap } from 'immutable';
-import React from 'react';
-
-import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
-import { normalizeAccount, normalizeInstance } from 'soapbox/normalizers';
-
-import Discover from '../discover';
-
-jest.mock('../../../hooks/useDimensions', () => ({
- useDimensions: () => [{ scrollWidth: 190 }, null, { width: 300 }],
-}));
-
-(window as any).ResizeObserver = class ResizeObserver {
-
- observe() { }
- disconnect() { }
-
-};
-
-const userId = '1';
-const store: any = {
- me: userId,
- accounts: ImmutableMap({
- [userId]: normalizeAccount({
- id: userId,
- acct: 'justin-username',
- display_name: 'Justin L',
- avatar: 'test.jpg',
- chats_onboarded: false,
- }),
- }),
- instance: normalizeInstance({
- version: '3.4.1 (compatible; TruthSocial 1.0.0)',
- software: 'TRUTHSOCIAL',
- }),
-};
-
-const renderApp = () => (
- render(
- ,
- undefined,
- store,
- )
-);
-
-describe('', () => {
- describe('before the user starts searching', () => {
- it('it should render popular groups', async () => {
- renderApp();
-
- await waitFor(() => {
- expect(screen.getByTestId('popular-groups')).toBeInTheDocument();
- expect(screen.getByTestId('suggested-groups')).toBeInTheDocument();
- expect(screen.getByTestId('popular-tags')).toBeInTheDocument();
- expect(screen.queryAllByTestId('recent-searches')).toHaveLength(0);
- expect(screen.queryAllByTestId('group-search-icon')).toHaveLength(0);
-
- });
- });
- });
-
- describe('when the user focuses on the input', () => {
- it('should render the search experience', async () => {
- const user = userEvent.setup();
- renderApp();
-
- await user.click(screen.getByTestId('search'));
-
- await waitFor(() => {
- expect(screen.getByTestId('group-search-icon')).toBeInTheDocument();
- expect(screen.getByTestId('recent-searches')).toBeInTheDocument();
- expect(screen.queryAllByTestId('popular-groups')).toHaveLength(0);
- });
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/__tests__/pending-requests.test.tsx b/app/soapbox/features/groups/__tests__/pending-requests.test.tsx
deleted file mode 100644
index fbc7aba3a..000000000
--- a/app/soapbox/features/groups/__tests__/pending-requests.test.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Map as ImmutableMap } from 'immutable';
-import React from 'react';
-import { VirtuosoMockContext } from 'react-virtuoso';
-
-import { __stub } from 'soapbox/api';
-import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
-import { normalizeAccount, normalizeGroup, normalizeGroupRelationship, normalizeInstance } from 'soapbox/normalizers';
-
-import PendingRequests from '../pending-requests';
-
-const userId = '1';
-const store: any = {
- me: userId,
- accounts: ImmutableMap({
- [userId]: normalizeAccount({
- id: userId,
- acct: 'justin-username',
- display_name: 'Justin L',
- avatar: 'test.jpg',
- chats_onboarded: false,
- }),
- }),
- instance: normalizeInstance({
- version: '3.4.1 (compatible; TruthSocial 1.0.0)',
- software: 'TRUTHSOCIAL',
- }),
-};
-
-const renderApp = () => (
- render(
-
-
- ,
- undefined,
- store,
- )
-);
-
-describe('', () => {
- describe('without pending group requests', () => {
- beforeEach(() => {
- __stub((mock) => {
- mock.onGet('/api/v1/groups?pending=true').reply(200, []);
- });
- });
-
- it('should render the blankslate', async () => {
- renderApp();
-
- await waitFor(() => {
- expect(screen.getByTestId('pending-requests-blankslate')).toBeInTheDocument();
- expect(screen.queryAllByTestId('group-card')).toHaveLength(0);
- });
- });
- });
-
- describe('with pending group requests', () => {
- beforeEach(() => {
- __stub((mock) => {
- mock.onGet('/api/v1/groups').reply(200, [
- normalizeGroup({
- display_name: 'Group',
- id: '1',
- }),
- ]);
-
- mock.onGet('/api/v1/groups/relationships?id[]=1').reply(200, [
- normalizeGroupRelationship({
- id: '1',
- }),
- ]);
- });
- });
-
- it('should render the groups', async () => {
- renderApp();
-
- await waitFor(() => {
- expect(screen.queryAllByTestId('group-card')).toHaveLength(1);
- expect(screen.queryAllByTestId('pending-requests-blankslate')).toHaveLength(0);
- });
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/__tests__/pending-group-rows.test.tsx b/app/soapbox/features/groups/components/__tests__/pending-group-rows.test.tsx
deleted file mode 100644
index 1b6cee612..000000000
--- a/app/soapbox/features/groups/components/__tests__/pending-group-rows.test.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { Map as ImmutableMap } from 'immutable';
-import React from 'react';
-import { VirtuosoMockContext } from 'react-virtuoso';
-
-import { __stub } from 'soapbox/api';
-import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
-import { normalizeAccount, normalizeGroup, normalizeGroupRelationship, normalizeInstance } from 'soapbox/normalizers';
-
-import PendingGroupsRow from '../pending-groups-row';
-
-const userId = '1';
-let store: any = {
- me: userId,
- accounts: ImmutableMap({
- [userId]: normalizeAccount({
- id: userId,
- acct: 'justin-username',
- display_name: 'Justin L',
- avatar: 'test.jpg',
- chats_onboarded: false,
- }),
- }),
-};
-
-const renderApp = (store: any) => (
- render(
-
-
- ,
- undefined,
- store,
- )
-);
-
-describe('', () => {
- describe('without the feature', () => {
- beforeEach(() => {
- store = {
- ...store,
- instance: normalizeInstance({
- version: '2.7.2 (compatible; Pleroma 2.3.0)',
- }),
- };
- });
-
- it('should not render', () => {
- renderApp(store);
- expect(screen.queryAllByTestId('pending-items-row')).toHaveLength(0);
- });
- });
-
- describe('with the feature', () => {
- beforeEach(() => {
- store = {
- ...store,
- instance: normalizeInstance({
- version: '3.4.1 (compatible; TruthSocial 1.0.0)',
- software: 'TRUTHSOCIAL',
- }),
- };
- });
-
- describe('without pending group requests', () => {
- beforeEach(() => {
- __stub((mock) => {
- mock.onGet('/api/v1/groups?pending=true').reply(200, []);
- });
- });
-
- it('should not render', () => {
- renderApp(store);
- expect(screen.queryAllByTestId('pending-items-row')).toHaveLength(0);
- });
- });
-
- describe('with pending group requests', () => {
- beforeEach(() => {
- __stub((mock) => {
- mock.onGet('/api/v1/groups').reply(200, [
- normalizeGroup({
- display_name: 'Group',
- id: '1',
- }),
- ]);
-
- mock.onGet('/api/v1/groups/relationships?id[]=1').reply(200, [
- normalizeGroupRelationship({
- id: '1',
- }),
- ]);
- });
- });
-
- it('should render the row', async () => {
- renderApp(store);
-
- await waitFor(() => {
- expect(screen.queryAllByTestId('pending-items-row')).toHaveLength(1);
- });
- });
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/__tests__/group-grid-item.test.tsx b/app/soapbox/features/groups/components/discover/__tests__/group-grid-item.test.tsx
deleted file mode 100644
index 09be8da84..000000000
--- a/app/soapbox/features/groups/components/discover/__tests__/group-grid-item.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-
-import { buildGroup } from 'soapbox/jest/factory';
-import { render, screen } from 'soapbox/jest/test-helpers';
-
-import GroupGridItem from '../group-grid-item';
-
-describe(' {
- it('should render correctly', () => {
- const group = buildGroup({
- display_name: 'group name here',
- locked: false,
- members_count: 6,
- });
- render();
-
- expect(screen.getByTestId('group-grid-item')).toHaveTextContent(group.display_name);
- expect(screen.getByTestId('group-grid-item')).toHaveTextContent('Public');
- expect(screen.getByTestId('group-grid-item')).toHaveTextContent('6 members');
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/__tests__/group-list-item.test.tsx b/app/soapbox/features/groups/components/discover/__tests__/group-list-item.test.tsx
deleted file mode 100644
index f78b9da43..000000000
--- a/app/soapbox/features/groups/components/discover/__tests__/group-list-item.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-
-import { buildGroup } from 'soapbox/jest/factory';
-import { render, screen } from 'soapbox/jest/test-helpers';
-
-import GroupListItem from '../group-list-item';
-
-describe(' {
- it('should render correctly', () => {
- const group = buildGroup({
- display_name: 'group name here',
- locked: false,
- members_count: 6,
- });
- render();
-
- expect(screen.getByTestId('group-list-item')).toHaveTextContent(group.display_name);
- expect(screen.getByTestId('group-list-item')).toHaveTextContent('Public');
- expect(screen.getByTestId('group-list-item')).toHaveTextContent('6 members');
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/__tests__/layout-buttons.test.tsx b/app/soapbox/features/groups/components/discover/__tests__/layout-buttons.test.tsx
deleted file mode 100644
index c6d1ea514..000000000
--- a/app/soapbox/features/groups/components/discover/__tests__/layout-buttons.test.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-
-import { render, screen, within } from 'soapbox/jest/test-helpers';
-
-import LayoutButtons, { GroupLayout } from '../layout-buttons';
-
-describe(' {
- describe('when LIST view', () => {
- it('should render correctly', async () => {
- const onSelectFn = jest.fn();
- const user = userEvent.setup();
-
- render();
-
- expect(within(screen.getByTestId('layout-list-action')).getByTestId('svg-icon-loader')).toHaveClass('text-primary-600');
- expect(within(screen.getByTestId('layout-grid-action')).getByTestId('svg-icon-loader')).not.toHaveClass('text-primary-600');
-
- await user.click(screen.getByTestId('layout-grid-action'));
- expect(onSelectFn).toHaveBeenCalled();
- });
- });
-
- describe('when GRID view', () => {
- it('should render correctly', async () => {
- const onSelectFn = jest.fn();
- const user = userEvent.setup();
-
- render();
-
- expect(within(screen.getByTestId('layout-list-action')).getByTestId('svg-icon-loader')).not.toHaveClass('text-primary-600');
- expect(within(screen.getByTestId('layout-grid-action')).getByTestId('svg-icon-loader')).toHaveClass('text-primary-600');
-
- await user.click(screen.getByTestId('layout-grid-action'));
- expect(onSelectFn).toHaveBeenCalled();
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/__tests__/tag-list-item.test.tsx b/app/soapbox/features/groups/components/discover/__tests__/tag-list-item.test.tsx
deleted file mode 100644
index c180c9234..000000000
--- a/app/soapbox/features/groups/components/discover/__tests__/tag-list-item.test.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-
-import { buildGroupTag } from 'soapbox/jest/factory';
-import { render, screen } from 'soapbox/jest/test-helpers';
-
-import TagListItem from '../tag-list-item';
-
-describe(' {
- it('should render correctly', () => {
- const tag = buildGroupTag({ name: 'tag 1', groups: 5 });
- render();
-
- expect(screen.getByTestId('tag-list-item')).toHaveTextContent(tag.name);
- expect(screen.getByTestId('tag-list-item')).toHaveTextContent('Number of groups: 5');
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/group-grid-item.tsx b/app/soapbox/features/groups/components/discover/group-grid-item.tsx
deleted file mode 100644
index 1f4920bde..000000000
--- a/app/soapbox/features/groups/components/discover/group-grid-item.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, { forwardRef } from 'react';
-import { Link } from 'react-router-dom';
-
-import GroupAvatar from 'soapbox/components/groups/group-avatar';
-import { HStack, Stack, Text } from 'soapbox/components/ui';
-import GroupActionButton from 'soapbox/features/group/components/group-action-button';
-import GroupHeaderImage from 'soapbox/features/group/components/group-header-image';
-import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
-import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
-
-import type { Group } from 'soapbox/schemas';
-
-interface IGroup {
- group: Group
- width?: number
-}
-
-const GroupGridItem = forwardRef((props: IGroup, ref: React.ForwardedRef) => {
- const { group, width = 'auto' } = props;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- •
-
-
-
-
-
-
-
-
-
- );
-});
-
-export default GroupGridItem;
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/group-list-item.tsx b/app/soapbox/features/groups/components/discover/group-list-item.tsx
deleted file mode 100644
index 6331d9d05..000000000
--- a/app/soapbox/features/groups/components/discover/group-list-item.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router-dom';
-
-import GroupAvatar from 'soapbox/components/groups/group-avatar';
-import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
-import GroupActionButton from 'soapbox/features/group/components/group-action-button';
-import { Group as GroupEntity } from 'soapbox/types/entities';
-import { shortNumberFormat } from 'soapbox/utils/numbers';
-
-interface IGroup {
- group: GroupEntity
- withJoinAction?: boolean
-}
-
-const GroupListItem = (props: IGroup) => {
- const { group, withJoinAction = true } = props;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {group.locked ? (
-
- ) : (
-
- )}
-
-
- {typeof group.members_count !== 'undefined' && (
- <>
- •
-
- {shortNumberFormat(group.members_count)}
- {' '}
-
-
- >
- )}
-
-
-
-
-
- {withJoinAction && (
-
- )}
-
- );
-};
-
-export default GroupListItem;
diff --git a/app/soapbox/features/groups/components/discover/layout-buttons.tsx b/app/soapbox/features/groups/components/discover/layout-buttons.tsx
deleted file mode 100644
index 9b98ba5e7..000000000
--- a/app/soapbox/features/groups/components/discover/layout-buttons.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import clsx from 'clsx';
-import React from 'react';
-
-import { HStack, Icon } from 'soapbox/components/ui';
-
-enum GroupLayout {
- LIST = 'LIST',
- GRID = 'GRID'
-}
-
-interface ILayoutButtons {
- layout: GroupLayout
- onSelect(layout: GroupLayout): void
-}
-
-const LayoutButtons = ({ layout, onSelect }: ILayoutButtons) => (
-
-
-
-
-
-);
-
-export { LayoutButtons as default, GroupLayout };
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/popular-groups.tsx b/app/soapbox/features/groups/components/discover/popular-groups.tsx
deleted file mode 100644
index 83426f553..000000000
--- a/app/soapbox/features/groups/components/discover/popular-groups.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-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 GroupGridItem from './group-grid-item';
-
-const PopularGroups = () => {
- const { groups, isFetching, isFetched, isError } = usePopularGroups();
- const isEmpty = (isFetched && groups.length === 0) || isError;
-
- const [groupCover, setGroupCover] = useState(null);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isEmpty ? (
-
-
-
- ) : (
-
- {({ width }: { width: number }) => (
- <>
- {isFetching ? (
- new Array(4).fill(0).map((_, idx) => (
-
- ))
- ) : (
- groups.map((group) => (
-
- ))
- )}
- >
- )}
-
- )}
-
- );
-};
-
-export default PopularGroups;
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/popular-tags.tsx b/app/soapbox/features/groups/components/discover/popular-tags.tsx
deleted file mode 100644
index 75ff36628..000000000
--- a/app/soapbox/features/groups/components/discover/popular-tags.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-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 TagListItem from './tag-list-item';
-
-const PopularTags = () => {
- const { tags, isFetched, isError } = usePopularTags();
- const isEmpty = (isFetched && tags.length === 0) || isError;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isEmpty ? (
-
-
-
- ) : (
-
- {tags.slice(0, 10).map((tag) => (
-
- ))}
-
- )}
-
- );
-};
-
-export default PopularTags;
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/search/__tests__/blankslate.test.tsx b/app/soapbox/features/groups/components/discover/search/__tests__/blankslate.test.tsx
deleted file mode 100644
index 6b1166477..000000000
--- a/app/soapbox/features/groups/components/discover/search/__tests__/blankslate.test.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-
-import { render, screen } from 'soapbox/jest/test-helpers';
-
-import Blankslate from '../blankslate';
-
-
-describe('', () => {
- describe('with string props', () => {
- it('should render correctly', () => {
- render();
-
- expect(screen.getByTestId('no-results')).toHaveTextContent('Title');
- expect(screen.getByTestId('no-results')).toHaveTextContent('Subtitle');
- });
- });
-
- describe('with node props', () => {
- it('should render correctly', () => {
- render(
- Title}
- subtitle={Subtitle}
- />);
-
- expect(screen.getByTestId('no-results')).toHaveTextContent('Title');
- expect(screen.getByTestId('no-results')).toHaveTextContent('Subtitle');
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/search/__tests__/recent-searches.test.tsx b/app/soapbox/features/groups/components/discover/search/__tests__/recent-searches.test.tsx
deleted file mode 100644
index 8c0e54262..000000000
--- a/app/soapbox/features/groups/components/discover/search/__tests__/recent-searches.test.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import userEvent from '@testing-library/user-event';
-import { Map as ImmutableMap } from 'immutable';
-import React from 'react';
-import { VirtuosoMockContext } from 'react-virtuoso';
-
-import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
-import { normalizeAccount } from 'soapbox/normalizers';
-import { groupSearchHistory } from 'soapbox/settings';
-import { clearRecentGroupSearches, saveGroupSearch } from 'soapbox/utils/groups';
-
-import RecentSearches from '../recent-searches';
-
-const userId = '1';
-const store = {
- me: userId,
- accounts: ImmutableMap({
- [userId]: normalizeAccount({
- id: userId,
- acct: 'justin-username',
- display_name: 'Justin L',
- avatar: 'test.jpg',
- chats_onboarded: false,
- }),
- }),
-};
-
-const renderApp = (children: React.ReactNode) => (
- render(
-
- {children}
- ,
- undefined,
- store,
- )
-);
-
-describe('', () => {
- describe('with recent searches', () => {
- beforeEach(() => {
- saveGroupSearch(userId, 'foobar');
- });
-
- afterEach(() => {
- clearRecentGroupSearches(userId);
- });
-
- it('should render the recent searches', async () => {
- renderApp();
-
- await waitFor(() => {
- expect(screen.getByTestId('recent-search')).toBeInTheDocument();
- });
- });
-
- it('should support clearing recent searches', async () => {
- renderApp();
-
- expect(groupSearchHistory.get(userId)).toHaveLength(1);
- await userEvent.click(screen.getByTestId('clear-recent-searches'));
- expect(groupSearchHistory.get(userId)).toBeNull();
- });
-
- it('should support click events on the results', async () => {
- const handler = jest.fn();
- renderApp();
- expect(handler.mock.calls.length).toEqual(0);
- await userEvent.click(screen.getByTestId('recent-search-result'));
- expect(handler.mock.calls.length).toEqual(1);
- });
- });
-
- describe('without recent searches', () => {
- it('should render the blankslate', async () => {
- renderApp();
-
- expect(screen.getByTestId('recent-searches-blankslate')).toBeInTheDocument();
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/search/__tests__/results.test.tsx b/app/soapbox/features/groups/components/discover/search/__tests__/results.test.tsx
deleted file mode 100644
index 66523ec5d..000000000
--- a/app/soapbox/features/groups/components/discover/search/__tests__/results.test.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import userEvent from '@testing-library/user-event';
-import { Map as ImmutableMap } from 'immutable';
-import React from 'react';
-import { VirtuosoGridMockContext, VirtuosoMockContext } from 'react-virtuoso';
-
-import { buildGroup } from 'soapbox/jest/factory';
-import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
-import { normalizeAccount } from 'soapbox/normalizers';
-
-import Results from '../results';
-
-const userId = '1';
-const store = {
- me: userId,
- accounts: ImmutableMap({
- [userId]: normalizeAccount({
- id: userId,
- acct: 'justin-username',
- display_name: 'Justin L',
- avatar: 'test.jpg',
- chats_onboarded: false,
- }),
- }),
-};
-
-const renderApp = (children: React.ReactNode) => (
- render(
-
-
- {children}
-
- ,
- undefined,
- store,
- )
-);
-
-const groupSearchResult = {
- groups: [buildGroup()],
- hasNextPage: false,
- isFetching: false,
- fetchNextPage: jest.fn(),
-} as any;
-
-describe('', () => {
- describe('with a list layout', () => {
- it('should render the GroupListItem components', async () => {
- renderApp();
- await waitFor(() => {
- expect(screen.getByTestId('group-list-item')).toBeInTheDocument();
- });
- });
- });
-
- describe('with a grid layout', () => {
- it('should render the GroupGridItem components', async () => {
- const user = userEvent.setup();
- renderApp();
-
- await user.click(screen.getByTestId('layout-grid-action'));
-
- await waitFor(() => {
- expect(screen.getByTestId('group-grid-item')).toBeInTheDocument();
- });
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/search/__tests__/search.test.tsx b/app/soapbox/features/groups/components/discover/search/__tests__/search.test.tsx
deleted file mode 100644
index 0c19eba01..000000000
--- a/app/soapbox/features/groups/components/discover/search/__tests__/search.test.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import React from 'react';
-
-import { __stub } from 'soapbox/api';
-import { buildGroup } from 'soapbox/jest/factory';
-import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
-import { normalizeInstance } from 'soapbox/normalizers';
-
-import Search from '../search';
-
-const store = {
- instance: normalizeInstance({
- version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
- }),
-};
-
-const renderApp = (children: React.ReactElement) => render(children, undefined, store);
-
-describe('', () => {
- describe('with no results', () => {
- beforeEach(() => {
- __stub((mock) => {
- mock.onGet('/api/v1/groups/search').reply(200, []);
- });
- });
-
- it('should render the blankslate', async () => {
- renderApp();
-
- await waitFor(() => {
- expect(screen.getByTestId('no-results')).toBeInTheDocument();
- });
- });
- });
-
- describe('with results', () => {
- beforeEach(() => {
- __stub((mock) => {
- mock.onGet('/api/v1/groups/search').reply(200, [
- buildGroup({
- display_name: 'Group',
- id: '1',
- }),
- ]);
- });
- });
-
- it('should render the results', async () => {
- renderApp();
-
- await waitFor(() => {
- expect(screen.getByTestId('results')).toBeInTheDocument();
- });
- });
- });
-
- describe('before starting a search', () => {
- it('should render the RecentSearches component', () => {
- renderApp();
-
- expect(screen.getByTestId('recent-searches')).toBeInTheDocument();
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/search/blankslate.tsx b/app/soapbox/features/groups/components/discover/search/blankslate.tsx
deleted file mode 100644
index efc179bd4..000000000
--- a/app/soapbox/features/groups/components/discover/search/blankslate.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-
-import { Stack, Text } from 'soapbox/components/ui';
-
-interface Props {
- title: React.ReactNode | string
- subtitle: React.ReactNode | string
-}
-
-export default ({ title, subtitle }: Props) => (
-
-
- {title}
-
-
-
- {subtitle}
-
-
-);
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/search/recent-searches.tsx b/app/soapbox/features/groups/components/discover/search/recent-searches.tsx
deleted file mode 100644
index 44f134b4c..000000000
--- a/app/soapbox/features/groups/components/discover/search/recent-searches.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React, { useState } from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Virtuoso } from 'react-virtuoso';
-
-import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
-import { useOwnAccount } from 'soapbox/hooks';
-import { groupSearchHistory } from 'soapbox/settings';
-import { clearRecentGroupSearches } from 'soapbox/utils/groups';
-
-interface Props {
- onSelect(value: string): void
-}
-
-export default (props: Props) => {
- const { onSelect } = props;
-
- const me = useOwnAccount();
-
- const [recentSearches, setRecentSearches] = useState(groupSearchHistory.get(me?.id as string) || []);
-
- const onClearRecentSearches = () => {
- clearRecentGroupSearches(me?.id as string);
- setRecentSearches([]);
- };
-
- return (
-
- {recentSearches.length > 0 ? (
- <>
-
-
-
-
-
-
-
-
- (
-
-
-
- )}
- />
- >
- ) : (
-
-
-
-
-
-
-
-
-
- )}
-
- );
-};
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/search/results.tsx b/app/soapbox/features/groups/components/discover/search/results.tsx
deleted file mode 100644
index 3ae6b2179..000000000
--- a/app/soapbox/features/groups/components/discover/search/results.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import clsx from 'clsx';
-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 GroupGridItem from '../group-grid-item';
-import GroupListItem from '../group-list-item';
-import LayoutButtons, { GroupLayout } from '../layout-buttons';
-
-import type { Group } from 'soapbox/types/entities';
-
-interface Props {
- groupSearchResult: ReturnType
-}
-
-const GridList: Components['List'] = React.forwardRef((props, ref) => {
- const { context, ...rest } = props;
- return ;
-});
-
-export default (props: Props) => {
- const { groupSearchResult } = props;
-
- const [layout, setLayout] = useState(GroupLayout.LIST);
-
- const { groups, hasNextPage, isFetching, fetchNextPage } = groupSearchResult;
-
- const handleLoadMore = () => {
- if (hasNextPage && !isFetching) {
- fetchNextPage();
- }
- };
-
- const renderGroupList = useCallback((group: Group, index: number) => (
-
-
-
- ), []);
-
- const renderGroupGrid = useCallback((group: Group) => (
-
- ), []);
-
- return (
-
-
-
-
-
-
- setLayout(selectedLayout)}
- />
-
-
- {layout === GroupLayout.LIST ? (
- renderGroupList(group, index)}
- endReached={handleLoadMore}
- />
- ) : (
- renderGroupGrid(group)}
- components={{
- Item: (props) => (
-
- ),
- List: GridList,
- }}
- endReached={handleLoadMore}
- />
- )}
-
- );
-};
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/search/search.tsx b/app/soapbox/features/groups/components/discover/search/search.tsx
deleted file mode 100644
index 0e2d7f00e..000000000
--- a/app/soapbox/features/groups/components/discover/search/search.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-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 { saveGroupSearch } from 'soapbox/utils/groups';
-
-import Blankslate from './blankslate';
-import RecentSearches from './recent-searches';
-import Results from './results';
-
-interface Props {
- onSelect(value: string): void
- searchValue: string
-}
-
-export default (props: Props) => {
- const { onSelect, searchValue } = props;
-
- const me = useOwnAccount();
- const debounce = useDebounce;
-
- const debouncedValue = debounce(searchValue as string, 300);
- const debouncedValueToSave = debounce(searchValue as string, 1000);
-
- const groupSearchResult = useGroupSearch(debouncedValue);
- const { groups, isLoading, isFetched, isError } = groupSearchResult;
-
- const hasSearchResults = isFetched && groups.length > 0;
- const hasNoSearchResults = isFetched && groups.length === 0;
-
- useEffect(() => {
- if (debouncedValueToSave && debouncedValueToSave.length >= 0) {
- saveGroupSearch(me?.id as string, debouncedValueToSave);
- }
- }, [debouncedValueToSave]);
-
- if (isLoading) {
- return (
-
-
-
-
-
- );
- }
-
- if (isError) {
- return (
-
- }
- subtitle={
-
- }
- />
- );
- }
-
- if (hasNoSearchResults) {
- return (
-
- }
- subtitle={
-
- }
- />
- );
- }
-
- if (hasSearchResults) {
- return (
-
- );
- }
-
- return (
-
- );
-};
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/suggested-groups.tsx b/app/soapbox/features/groups/components/discover/suggested-groups.tsx
deleted file mode 100644
index 5d73cc3f0..000000000
--- a/app/soapbox/features/groups/components/discover/suggested-groups.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-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 GroupGridItem from './group-grid-item';
-
-const SuggestedGroups = () => {
- const { groups, isFetching, isFetched, isError } = useSuggestedGroups();
- const isEmpty = (isFetched && groups.length === 0) || isError;
-
- const [groupCover, setGroupCover] = useState(null);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isEmpty ? (
-
-
-
- ) : (
-
- {({ width }: { width: number }) => (
- <>
- {isFetching ? (
- new Array(20).fill(0).map((_, idx) => (
-
- ))
- ) : (
- groups.map((group) => (
-
- ))
- )}
- >
- )}
-
- )}
-
- );
-};
-
-export default SuggestedGroups;
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/discover/tag-list-item.tsx b/app/soapbox/features/groups/components/discover/tag-list-item.tsx
deleted file mode 100644
index 1d11ce267..000000000
--- a/app/soapbox/features/groups/components/discover/tag-list-item.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router-dom';
-
-import { Stack, Text } from 'soapbox/components/ui';
-
-import type { GroupTag } from 'soapbox/schemas';
-
-interface ITagListItem {
- tag: GroupTag
-}
-
-const TagListItem = (props: ITagListItem) => {
- const { tag } = props;
-
- return (
-
-
-
- #{tag.name}
-
-
-
-
- :{' '}
- {tag.groups}
-
-
-
- );
-};
-
-export default TagListItem;
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/group-link-preview.tsx b/app/soapbox/features/groups/components/group-link-preview.tsx
deleted file mode 100644
index 98ca03076..000000000
--- a/app/soapbox/features/groups/components/group-link-preview.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react';
-import { useHistory } from 'react-router-dom';
-
-import { Avatar, Button, CardTitle, Stack } from 'soapbox/components/ui';
-import { type Card as StatusCard } from 'soapbox/types/entities';
-
-interface IGroupLinkPreview {
- card: StatusCard
-}
-
-const GroupLinkPreview: React.FC = ({ card }) => {
- const history = useHistory();
-
- const { group } = card;
- if (!group) return null;
-
- const navigateToGroup = () => history.push(`/group/${group.slug}`);
-
- return (
-
-
-
-
-
-
- } />
-
-
-
-
- );
-};
-
-export { GroupLinkPreview };
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/pending-groups-row.tsx b/app/soapbox/features/groups/components/pending-groups-row.tsx
deleted file mode 100644
index 4d2760760..000000000
--- a/app/soapbox/features/groups/components/pending-groups-row.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react';
-
-import { PendingItemsRow } from 'soapbox/components/pending-items-row';
-import { Divider } from 'soapbox/components/ui';
-import { useFeatures } from 'soapbox/hooks';
-import { usePendingGroups } from 'soapbox/queries/groups';
-
-export default () => {
- const features = useFeatures();
-
- const { groups, isFetching } = usePendingGroups();
-
- if (!features.groupsPending || isFetching || groups.length === 0) {
- return null;
- }
-
- return (
- <>
-
-
-
- >
- );
-};
\ No newline at end of file
diff --git a/app/soapbox/features/groups/components/tab-bar.tsx b/app/soapbox/features/groups/components/tab-bar.tsx
deleted file mode 100644
index 7a342bfc8..000000000
--- a/app/soapbox/features/groups/components/tab-bar.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, { useMemo } from 'react';
-import { useHistory } from 'react-router-dom';
-
-import { Tabs } from 'soapbox/components/ui';
-
-import type { Item } from 'soapbox/components/ui/tabs/tabs';
-
-export enum TabItems {
- MY_GROUPS = 'MY_GROUPS',
- FIND_GROUPS = 'FIND_GROUPS'
-}
-
-interface ITabBar {
- activeTab: TabItems
-}
-
-const TabBar = ({ activeTab }: ITabBar) => {
- const history = useHistory();
-
- const tabItems: Item[] = useMemo(() => ([
- {
- text: 'My Groups',
- action: () => history.push('/groups'),
- name: TabItems.MY_GROUPS,
- },
- {
- text: 'Find Groups',
- action: () => history.push('/groups/discover'),
- name: TabItems.FIND_GROUPS,
- },
- ]), []);
-
- return (
-
- );
-};
-
-export default TabBar;
\ No newline at end of file
diff --git a/app/soapbox/features/groups/discover.tsx b/app/soapbox/features/groups/discover.tsx
deleted file mode 100644
index 47273d2ed..000000000
--- a/app/soapbox/features/groups/discover.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React, { useState } from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-
-import { HStack, Icon, IconButton, Input, Stack } from 'soapbox/components/ui';
-
-import PopularGroups from './components/discover/popular-groups';
-import PopularTags from './components/discover/popular-tags';
-import Search from './components/discover/search/search';
-import SuggestedGroups from './components/discover/suggested-groups';
-import TabBar, { TabItems } from './components/tab-bar';
-
-const messages = defineMessages({
- placeholder: { id: 'groups.discover.search.placeholder', defaultMessage: 'Search' },
-});
-
-const Discover: React.FC = () => {
- const intl = useIntl();
-
- const [isSearching, setIsSearching] = useState(false);
- const [value, setValue] = useState('');
-
- const hasSearchValue = value && value.length > 0;
-
- const cancelSearch = () => {
- clearValue();
- setIsSearching(false);
- };
-
- const clearValue = () => setValue('');
-
- return (
-
-
-
-
-
- {isSearching ? (
-
- ) : null}
-
- setValue(event.target.value)}
- onFocus={() => setIsSearching(true)}
- outerClassName='mt-0 w-full'
- theme='search'
- append={
-
- }
- />
-
-
- {isSearching ? (
- setValue(newValue)}
- />
- ) : (
- <>
-
-
-
- >
- )}
-
-
- );
-};
-
-export default Discover;
diff --git a/app/soapbox/features/groups/index.tsx b/app/soapbox/features/groups/index.tsx
deleted file mode 100644
index 7b1c51c55..000000000
--- a/app/soapbox/features/groups/index.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import React, { useState } from 'react';
-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 { PERMISSION_CREATE_GROUPS, hasPermission } from 'soapbox/utils/permissions';
-
-import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card';
-
-import PendingGroupsRow from './components/pending-groups-row';
-import TabBar, { TabItems } from './components/tab-bar';
-
-const messages = defineMessages({
- placeholder: { id: 'groups.search.placeholder', defaultMessage: 'Search My Groups' },
-});
-
-const Groups: React.FC = () => {
- const debounce = useDebounce;
- const dispatch = useAppDispatch();
- const features = useFeatures();
- const intl = useIntl();
-
- const canCreateGroup = useAppSelector((state) => hasPermission(state, PERMISSION_CREATE_GROUPS));
-
- const [searchValue, setSearchValue] = useState('');
- const debouncedValue = debounce(searchValue, 300);
-
- const { groups, isLoading, hasNextPage, fetchNextPage } = useGroups(debouncedValue);
-
- const handleLoadMore = () => {
- if (hasNextPage) {
- fetchNextPage();
- }
- };
-
- const createGroup = () => dispatch(openModal('CREATE_GROUP'));
-
- const renderBlankslate = () => (
-
-
-
-
-
-
-
-
-
-
-
- {canCreateGroup && (
-
- )}
-
- );
-
- return (
-
- {features.groupsDiscovery && (
-
- )}
-
- {canCreateGroup && (
-
- )}
-
- {features.groupsSearch ? (
- setSearchValue(event.target.value)}
- placeholder={intl.formatMessage(messages.placeholder)}
- theme='search'
- value={searchValue}
- />
- ) : null}
-
-
-
-
- {groups.map((group) => (
-
-
-
- ))}
-
-
- );
-};
-
-export default Groups;
diff --git a/app/soapbox/features/groups/pending-requests.tsx b/app/soapbox/features/groups/pending-requests.tsx
deleted file mode 100644
index 1233ff3a7..000000000
--- a/app/soapbox/features/groups/pending-requests.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-import { Link } from 'react-router-dom';
-
-import GroupCard from 'soapbox/components/group-card';
-import ScrollableList from 'soapbox/components/scrollable-list';
-import { Column, Stack, Text } from 'soapbox/components/ui';
-import { usePendingGroups } from 'soapbox/queries/groups';
-
-import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card';
-
-const messages = defineMessages({
- label: { id: 'groups.pending.label', defaultMessage: 'Pending Requests' },
-});
-
-export default () => {
- const intl = useIntl();
-
- const { groups, isLoading } = usePendingGroups();
-
- const renderBlankslate = () => (
-
-
-
-
-
-
-
-
-
-
-
- );
-
- return (
-
-
- {groups.map((group) => (
-
-
-
- ))}
-
-
- );
-};
\ No newline at end of file
diff --git a/app/soapbox/features/groups/popular.tsx b/app/soapbox/features/groups/popular.tsx
deleted file mode 100644
index 2f417dd8f..000000000
--- a/app/soapbox/features/groups/popular.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import clsx from 'clsx';
-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 GroupGridItem from './components/discover/group-grid-item';
-import GroupListItem from './components/discover/group-list-item';
-import LayoutButtons, { GroupLayout } from './components/discover/layout-buttons';
-
-import type { Group } from 'soapbox/schemas';
-
-const messages = defineMessages({
- label: { id: 'groups.popular.label', defaultMessage: 'Popular Groups' },
-});
-
-const GridList: Components['List'] = React.forwardRef((props, ref) => {
- const { context, ...rest } = props;
- return ;
-});
-
-const Popular: React.FC = () => {
- const intl = useIntl();
-
- const [layout, setLayout] = useState(GroupLayout.LIST);
-
- const { groups, hasNextPage, fetchNextPage } = usePopularGroups();
-
- const handleLoadMore = () => {
- if (hasNextPage) {
- fetchNextPage();
- }
- };
-
- const renderGroupList = useCallback((group: Group, index: number) => (
-
-
-
- ), []);
-
- const renderGroupGrid = useCallback((group: Group) => (
-
- ), []);
-
- return (
- setLayout(selectedLayout)}
- />
- }
- >
- {layout === GroupLayout.LIST ? (
- renderGroupList(group, index)}
- endReached={handleLoadMore}
- />
- ) : (
- renderGroupGrid(group)}
- components={{
- Item: (props) => (
-
- ),
- List: GridList,
- }}
- endReached={handleLoadMore}
- />
- )}
-
- );
-};
-
-export default Popular;
diff --git a/app/soapbox/features/groups/suggested.tsx b/app/soapbox/features/groups/suggested.tsx
deleted file mode 100644
index 8a3e570e0..000000000
--- a/app/soapbox/features/groups/suggested.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import clsx from 'clsx';
-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 GroupGridItem from './components/discover/group-grid-item';
-import GroupListItem from './components/discover/group-list-item';
-import LayoutButtons, { GroupLayout } from './components/discover/layout-buttons';
-
-import type { Group } from 'soapbox/schemas';
-
-const messages = defineMessages({
- label: { id: 'groups.suggested.label', defaultMessage: 'Suggested Groups' },
-});
-
-const GridList: Components['List'] = React.forwardRef((props, ref) => {
- const { context, ...rest } = props;
- return ;
-});
-
-const Suggested: React.FC = () => {
- const intl = useIntl();
-
- const [layout, setLayout] = useState(GroupLayout.LIST);
-
- const { groups, hasNextPage, fetchNextPage } = useSuggestedGroups();
-
- const handleLoadMore = () => {
- if (hasNextPage) {
- fetchNextPage();
- }
- };
-
- const renderGroupList = useCallback((group: Group, index: number) => (
-
-
-
- ), []);
-
- const renderGroupGrid = useCallback((group: Group) => (
-
- ), []);
-
- return (
- setLayout(selectedLayout)}
- />
- }
- >
- {layout === GroupLayout.LIST ? (
- renderGroupList(group, index)}
- endReached={handleLoadMore}
- />
- ) : (
- renderGroupGrid(group)}
- components={{
- Item: (props) => (
-
- ),
- List: GridList,
- }}
- endReached={handleLoadMore}
- />
- )}
-
- );
-};
-
-export default Suggested;
diff --git a/app/soapbox/features/groups/tag.tsx b/app/soapbox/features/groups/tag.tsx
deleted file mode 100644
index ccc54bbb3..000000000
--- a/app/soapbox/features/groups/tag.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-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 GroupGridItem from './components/discover/group-grid-item';
-import GroupListItem from './components/discover/group-list-item';
-
-import type { Group } from 'soapbox/schemas';
-
-enum Layout {
- LIST = 'LIST',
- GRID = 'GRID'
-}
-
-const GridList: Components['List'] = React.forwardRef((props, ref) => {
- const { context, ...rest } = props;
- return ;
-});
-
-interface ITag {
- params: { id: string }
-}
-
-const Tag: React.FC = (props) => {
- const tagId = props.params.id;
-
- const [layout, setLayout] = useState(Layout.LIST);
-
- const { tag, isLoading } = useGroupTag(tagId);
- const { groups, hasNextPage, fetchNextPage } = useGroupsFromTag(tagId);
-
- const handleLoadMore = () => {
- if (hasNextPage) {
- fetchNextPage();
- }
- };
-
- const renderGroupList = useCallback((group: Group, index: number) => (
-
-
-
- ), []);
-
- const renderGroupGrid = useCallback((group: Group) => (
-
- ), []);
-
- if (isLoading || !tag) {
- return null;
- }
-
- return (
-
-
-
-
-
- }
- >
- {layout === Layout.LIST ? (
- renderGroupList(group, index)}
- endReached={handleLoadMore}
- />
- ) : (
- renderGroupGrid(group)}
- components={{
- Item: (props) => (
-
- ),
- List: GridList,
- }}
- endReached={handleLoadMore}
- />
- )}
-
- );
-};
-
-export default Tag;
diff --git a/app/soapbox/features/groups/tags.tsx b/app/soapbox/features/groups/tags.tsx
deleted file mode 100644
index aa37a514b..000000000
--- a/app/soapbox/features/groups/tags.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import clsx from 'clsx';
-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 TagListItem from './components/discover/tag-list-item';
-
-import type { GroupTag } from 'soapbox/schemas';
-
-const messages = defineMessages({
- title: { id: 'groups.tags.title', defaultMessage: 'Browse Topics' },
-});
-
-const Tags: React.FC = () => {
- const intl = useIntl();
-
- const { tags, isFetched, isError, hasNextPage, fetchNextPage } = usePopularTags();
- const isEmpty = (isFetched && tags.length === 0) || isError;
-
- const handleLoadMore = () => {
- if (hasNextPage) {
- fetchNextPage();
- }
- };
-
- const renderItem = (index: number, tag: GroupTag) => (
-
-
-
- );
-
- return (
-
- {isEmpty ? (
-
-
-
- ) : (
-
- )}
-
- );
-};
-
-export default Tags;
diff --git a/app/soapbox/features/placeholder/components/placeholder-group-card.tsx b/app/soapbox/features/placeholder/components/placeholder-group-card.tsx
deleted file mode 100644
index b43ae1aaf..000000000
--- a/app/soapbox/features/placeholder/components/placeholder-group-card.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-
-import { HStack, Stack, Text } from 'soapbox/components/ui';
-
-import { generateText, randomIntFromInterval } from '../utils';
-
-const PlaceholderGroupCard = () => {
- const groupNameLength = randomIntFromInterval(12, 20);
-
- return (
-
-
- {/* Group Cover Image */}
-
-
- {/* Group Avatar */}
-
-
- {/* Group Info */}
-
- {generateText(groupNameLength)}
-
-
- {generateText(6)}
- {generateText(6)}
-
-
-
-
- );
-};
-
-export default PlaceholderGroupCard;
diff --git a/app/soapbox/features/placeholder/components/placeholder-group-discover.tsx b/app/soapbox/features/placeholder/components/placeholder-group-discover.tsx
deleted file mode 100644
index 468cb5481..000000000
--- a/app/soapbox/features/placeholder/components/placeholder-group-discover.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-
-import { HStack, Stack, Text } from 'soapbox/components/ui';
-
-import { generateText, randomIntFromInterval } from '../utils';
-
-const PlaceholderGroupDiscover = () => {
- const groupNameLength = randomIntFromInterval(12, 20);
-
- return (
-
-
- {/* Group Cover Image */}
-
-
-
- {/* Group Avatar */}
-
-
- {/* Group Info */}
-
- {generateText(groupNameLength)}
-
-
- {generateText(6)}
- {generateText(6)}
-
-
-
-
-
- {/* Join Group Button */}
-
-
- );
-};
-
-export default PlaceholderGroupDiscover;
diff --git a/app/soapbox/features/placeholder/components/placeholder-group-search.tsx b/app/soapbox/features/placeholder/components/placeholder-group-search.tsx
deleted file mode 100644
index 3b3bd3870..000000000
--- a/app/soapbox/features/placeholder/components/placeholder-group-search.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-
-import { HStack, Stack, Text } from 'soapbox/components/ui';
-
-import { generateText, randomIntFromInterval } from '../utils';
-
-export default ({ withJoinAction = true }: { withJoinAction?: boolean }) => {
- const groupNameLength = randomIntFromInterval(12, 20);
-
- return (
-
-
- {/* Group Avatar */}
-
-
-
-
- {generateText(groupNameLength)}
-
-
-
-
- {generateText(6)}
-
-
- •
-
-
- {generateText(6)}
-
-
-
-
-
- {/* Join Group Button */}
- {withJoinAction && (
-
- )}
-
- );
-};
diff --git a/app/soapbox/features/public-layout/components/__tests__/header.test.tsx b/app/soapbox/features/public-layout/components/__tests__/header.test.tsx
deleted file mode 100644
index d69198efa..000000000
--- a/app/soapbox/features/public-layout/components/__tests__/header.test.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react';
-
-import { storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
-import { render, screen } from 'soapbox/jest/test-helpers';
-
-import Header from '../header';
-
-describe('', () => {
- it('successfully renders', () => {
- render();
- expect(screen.getByTestId('public-layout-header')).toBeInTheDocument();
- });
-
- it('doesn\'t display the signup button by default', () => {
- render();
- expect(screen.queryByText('Register')).not.toBeInTheDocument();
- });
-
- describe('with registrations enabled', () => {
- it('displays the signup button', () => {
- render(, undefined, storeOpen);
- expect(screen.getByText('Register')).toBeInTheDocument();
- });
- });
-
- describe('with registrations closed, Pepe enabled', () => {
- it('displays the signup button', () => {
- render(, undefined, storePepeOpen);
- expect(screen.getByText('Register')).toBeInTheDocument();
- });
- });
-});
\ No newline at end of file
diff --git a/app/soapbox/features/status/components/thread.tsx b/app/soapbox/features/status/components/thread.tsx
deleted file mode 100644
index aa563e839..000000000
--- a/app/soapbox/features/status/components/thread.tsx
+++ /dev/null
@@ -1,468 +0,0 @@
-import { createSelector } from '@reduxjs/toolkit';
-import clsx from 'clsx';
-import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
-import React, { useEffect, useRef, useState } from 'react';
-import { HotKeys } from 'react-hotkeys';
-import { useIntl } from 'react-intl';
-import { useHistory } from 'react-router-dom';
-import { type VirtuosoHandle } from 'react-virtuoso';
-
-import { mentionCompose, replyCompose } from 'soapbox/actions/compose';
-import { favourite, reblog, unfavourite, unreblog } from 'soapbox/actions/interactions';
-import { openModal } from 'soapbox/actions/modals';
-import { getSettings } from 'soapbox/actions/settings';
-import { hideStatus, revealStatus } from 'soapbox/actions/statuses';
-import ScrollableList from 'soapbox/components/scrollable-list';
-import StatusActionBar from 'soapbox/components/status-action-bar';
-import Tombstone from 'soapbox/components/tombstone';
-import { Stack } from 'soapbox/components/ui';
-import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status';
-import PendingStatus from 'soapbox/features/ui/components/pending-status';
-import { useAppDispatch, useAppSelector, useOwnAccount, useSettings } from 'soapbox/hooks';
-import { RootState } from 'soapbox/store';
-import { type Account, type Status } from 'soapbox/types/entities';
-import { defaultMediaVisibility, textForScreenReader } from 'soapbox/utils/status';
-
-import DetailedStatus from './detailed-status';
-import ThreadLoginCta from './thread-login-cta';
-import ThreadStatus from './thread-status';
-
-type DisplayMedia = 'default' | 'hide_all' | 'show_all';
-
-const getAncestorsIds = createSelector([
- (_: RootState, statusId: string | undefined) => statusId,
- (state: RootState) => state.contexts.inReplyTos,
-], (statusId, inReplyTos) => {
- let ancestorsIds = ImmutableOrderedSet();
- let id: string | undefined = statusId;
-
- while (id && !ancestorsIds.includes(id)) {
- ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds);
- id = inReplyTos.get(id);
- }
-
- return ancestorsIds;
-});
-
-export const getDescendantsIds = createSelector([
- (_: RootState, statusId: string) => statusId,
- (state: RootState) => state.contexts.replies,
-], (statusId, contextReplies) => {
- let descendantsIds = ImmutableOrderedSet();
- const ids = [statusId];
-
- while (ids.length > 0) {
- const id = ids.shift();
- if (!id) break;
-
- const replies = contextReplies.get(id);
-
- if (descendantsIds.includes(id)) {
- break;
- }
-
- if (statusId !== id) {
- descendantsIds = descendantsIds.union([id]);
- }
-
- if (replies) {
- replies.reverse().forEach((reply: string) => {
- ids.unshift(reply);
- });
- }
- }
-
- return descendantsIds;
-});
-
-interface IThread {
- status: Status
- withMedia?: boolean
- useWindowScroll?: boolean
- itemClassName?: string
- next: string | undefined
- handleLoadMore: () => void
-}
-
-const Thread = (props: IThread) => {
- const {
- handleLoadMore,
- itemClassName,
- next,
- status,
- useWindowScroll = true,
- withMedia = true,
- } = props;
-
- const dispatch = useAppDispatch();
- const history = useHistory();
- const intl = useIntl();
- const me = useOwnAccount();
- const settings = useSettings();
-
- const displayMedia = settings.get('displayMedia') as DisplayMedia;
- const isUnderReview = status?.visibility === 'self';
-
- const { ancestorsIds, descendantsIds } = useAppSelector((state) => {
- let ancestorsIds = ImmutableOrderedSet();
- let descendantsIds = ImmutableOrderedSet();
-
- if (status) {
- const statusId = status.id;
- ancestorsIds = getAncestorsIds(state, state.contexts.inReplyTos.get(statusId));
- descendantsIds = getDescendantsIds(state, statusId);
- ancestorsIds = ancestorsIds.delete(statusId).subtract(descendantsIds);
- descendantsIds = descendantsIds.delete(statusId).subtract(ancestorsIds);
- }
-
- return {
- status,
- ancestorsIds,
- descendantsIds,
- };
- });
-
- const [showMedia, setShowMedia] = useState(status?.visibility === 'self' ? false : defaultMediaVisibility(status, displayMedia));
-
- const node = useRef(null);
- const statusRef = useRef(null);
- const scroller = useRef(null);
-
- const handleToggleMediaVisibility = () => {
- setShowMedia(!showMedia);
- };
-
- const handleHotkeyReact = () => {
- if (statusRef.current) {
- const firstEmoji: HTMLButtonElement | null = statusRef.current.querySelector('.emoji-react-selector .emoji-react-selector__emoji');
- firstEmoji?.focus();
- }
- };
-
- const handleFavouriteClick = (status: Status) => {
- if (status.favourited) {
- dispatch(unfavourite(status));
- } else {
- dispatch(favourite(status));
- }
- };
-
- const handleReplyClick = (status: Status) => dispatch(replyCompose(status));
-
- const handleModalReblog = (status: Status) => dispatch(reblog(status));
-
- const handleReblogClick = (status: Status, e?: React.MouseEvent) => {
- dispatch((_, getState) => {
- const boostModal = getSettings(getState()).get('boostModal');
- if (status.reblogged) {
- dispatch(unreblog(status));
- } else {
- if ((e && e.shiftKey) || !boostModal) {
- handleModalReblog(status);
- } else {
- dispatch(openModal('BOOST', { status, onReblog: handleModalReblog }));
- }
- }
- });
- };
-
- const handleMentionClick = (account: Account) => dispatch(mentionCompose(account));
-
- const handleHotkeyOpenMedia = (e?: KeyboardEvent) => {
- const media = status?.media_attachments;
-
- e?.preventDefault();
-
- if (media && media.size) {
- const firstAttachment = media.first()!;
-
- if (media.size === 1 && firstAttachment.type === 'video') {
- dispatch(openModal('VIDEO', { media: firstAttachment, status: status }));
- } else {
- dispatch(openModal('MEDIA', { media, index: 0, status: status }));
- }
- }
- };
-
- const handleToggleHidden = (status: Status) => {
- if (status.hidden) {
- dispatch(revealStatus(status.id));
- } else {
- dispatch(hideStatus(status.id));
- }
- };
-
- const handleHotkeyMoveUp = () => {
- handleMoveUp(status!.id);
- };
-
- const handleHotkeyMoveDown = () => {
- handleMoveDown(status!.id);
- };
-
- const handleHotkeyReply = (e?: KeyboardEvent) => {
- e?.preventDefault();
- handleReplyClick(status!);
- };
-
- const handleHotkeyFavourite = () => {
- handleFavouriteClick(status!);
- };
-
- const handleHotkeyBoost = () => {
- handleReblogClick(status!);
- };
-
- const handleHotkeyMention = (e?: KeyboardEvent) => {
- e?.preventDefault();
- const { account } = status!;
- if (!account || typeof account !== 'object') return;
- handleMentionClick(account);
- };
-
- const handleHotkeyOpenProfile = () => {
- history.push(`/@${status!.getIn(['account', 'acct'])}`);
- };
-
- const handleHotkeyToggleHidden = () => {
- handleToggleHidden(status!);
- };
-
- const handleHotkeyToggleSensitive = () => {
- handleToggleMediaVisibility();
- };
-
- const handleMoveUp = (id: string) => {
- if (id === status?.id) {
- _selectChild(ancestorsIds.size - 1);
- } else {
- let index = ImmutableList(ancestorsIds).indexOf(id);
-
- if (index === -1) {
- index = ImmutableList(descendantsIds).indexOf(id);
- _selectChild(ancestorsIds.size + index);
- } else {
- _selectChild(index - 1);
- }
- }
- };
-
- const handleMoveDown = (id: string) => {
- if (id === status?.id) {
- _selectChild(ancestorsIds.size + 1);
- } else {
- let index = ImmutableList(ancestorsIds).indexOf(id);
-
- if (index === -1) {
- index = ImmutableList(descendantsIds).indexOf(id);
- _selectChild(ancestorsIds.size + index + 2);
- } else {
- _selectChild(index + 1);
- }
- }
- };
-
- const _selectChild = (index: number) => {
- scroller.current?.scrollIntoView({
- index,
- behavior: 'smooth',
- done: () => {
- const element = document.querySelector(`#thread [data-index="${index}"] .focusable`);
-
- if (element) {
- element.focus();
- }
- },
- });
- };
-
- const renderTombstone = (id: string) => {
- return (
-
-
-
- );
- };
-
- const renderStatus = (id: string) => {
- return (
-
- );
- };
-
- const renderPendingStatus = (id: string) => {
- const idempotencyKey = id.replace(/^末pending-/, '');
-
- return (
-
- );
- };
-
- const renderChildren = (list: ImmutableOrderedSet) => {
- return list.map(id => {
- if (id.endsWith('-tombstone')) {
- return renderTombstone(id);
- } else if (id.startsWith('末pending-')) {
- return renderPendingStatus(id);
- } else {
- return renderStatus(id);
- }
- });
- };
-
- // Reset media visibility if status changes.
- useEffect(() => {
- setShowMedia(status?.visibility === 'self' ? false : defaultMediaVisibility(status, displayMedia));
- }, [status.id]);
-
- // Scroll focused status into view when thread updates.
- useEffect(() => {
- scroller.current?.scrollToIndex({
- index: ancestorsIds.size,
- offset: -146,
- });
-
- setImmediate(() => statusRef.current?.querySelector('.detailed-actualStatus')?.focus());
- }, [status.id, ancestorsIds.size]);
-
- const handleOpenCompareHistoryModal = (status: Status) => {
- dispatch(openModal('COMPARE_HISTORY', {
- statusId: status.id,
- }));
- };
-
- const hasAncestors = ancestorsIds.size > 0;
- const hasDescendants = descendantsIds.size > 0;
-
- type HotkeyHandlers = { [key: string]: (keyEvent?: KeyboardEvent) => void };
-
- const handlers: HotkeyHandlers = {
- moveUp: handleHotkeyMoveUp,
- moveDown: handleHotkeyMoveDown,
- reply: handleHotkeyReply,
- favourite: handleHotkeyFavourite,
- boost: handleHotkeyBoost,
- mention: handleHotkeyMention,
- openProfile: handleHotkeyOpenProfile,
- toggleHidden: handleHotkeyToggleHidden,
- toggleSensitive: handleHotkeyToggleSensitive,
- openMedia: handleHotkeyOpenMedia,
- react: handleHotkeyReact,
- };
-
- const focusedStatus = (
-
-
-
-
-
-
- {!isUnderReview ? (
- <>
-
-
-
- >
- ) : null}
-
-
-
- {hasDescendants && (
-
- )}
-
- );
-
- const children: JSX.Element[] = [];
-
- if (!useWindowScroll) {
- // Add padding to the top of the Thread (for Media Modal)
- children.push();
- }
-
- if (hasAncestors) {
- children.push(...renderChildren(ancestorsIds).toArray());
- }
-
- children.push(focusedStatus);
-
- if (hasDescendants) {
- children.push(...renderChildren(descendantsIds).toArray());
- }
-
- return (
-
-
-
}
- initialTopMostItemIndex={ancestorsIds.size}
- useWindowScroll={useWindowScroll}
- itemClassName={itemClassName}
- className={
- clsx({
- 'h-full': !useWindowScroll,
- })
- }
- >
- {children}
-
-
-
- {!me && }
-
- );
-};
-
-export default Thread;
\ No newline at end of file
diff --git a/app/soapbox/features/ui/components/__tests__/navbar.test.tsx b/app/soapbox/features/ui/components/__tests__/navbar.test.tsx
deleted file mode 100644
index f46d9db72..000000000
--- a/app/soapbox/features/ui/components/__tests__/navbar.test.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react';
-
-import { storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
-import { render, screen } from 'soapbox/jest/test-helpers';
-
-import Navbar from '../navbar';
-
-describe('', () => {
- it('successfully renders', () => {
- render();
- expect(screen.getByTestId('navbar')).toBeInTheDocument();
- });
-
- it('doesn\'t display the signup button by default', () => {
- render();
- expect(screen.queryByText('Sign up')).not.toBeInTheDocument();
- });
-
- describe('with registrations enabled', () => {
- it('displays the signup button', () => {
- render(, undefined, storeOpen);
- expect(screen.getByText('Sign up')).toBeInTheDocument();
- });
- });
-
- describe('with registrations closed, Pepe enabled', () => {
- it('displays the signup button', () => {
- render(, undefined, storePepeOpen);
- expect(screen.getByText('Sign up')).toBeInTheDocument();
- });
- });
-});
diff --git a/app/soapbox/features/ui/components/group-media-panel.tsx b/app/soapbox/features/ui/components/group-media-panel.tsx
deleted file mode 100644
index 9f52af5a2..000000000
--- a/app/soapbox/features/ui/components/group-media-panel.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { List as ImmutableList } from 'immutable';
-import React, { useState, useEffect } from 'react';
-import { FormattedMessage } from 'react-intl';
-
-import { openModal } from 'soapbox/actions/modals';
-import { expandGroupMediaTimeline } from 'soapbox/actions/timelines';
-import { Spinner, Text, Widget } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-import { getGroupGallery } from 'soapbox/selectors';
-
-import MediaItem from '../../account-gallery/components/media-item';
-
-import type { Attachment, Group } from 'soapbox/types/entities';
-
-interface IGroupMediaPanel {
- group?: Group
-}
-
-const GroupMediaPanel: React.FC = ({ group }) => {
- const dispatch = useAppDispatch();
-
- const [loading, setLoading] = useState(true);
-
- const isMember = !!group?.relationship?.member;
- const isPrivate = group?.locked;
-
- const attachments: ImmutableList = useAppSelector((state) => group ? getGroupGallery(state, group?.id) : ImmutableList());
-
- const handleOpenMedia = (attachment: Attachment): void => {
- if (attachment.type === 'video') {
- dispatch(openModal('VIDEO', { media: attachment, status: attachment.status }));
- } else {
- const media = attachment.getIn(['status', 'media_attachments']) as ImmutableList;
- const index = media.findIndex(x => x.id === attachment.id);
-
- dispatch(openModal('MEDIA', { media, index, status: attachment.status, account: attachment.account }));
- }
- };
-
- useEffect(() => {
- setLoading(true);
-
- if (group && !group.deleted_at && (isMember || !isPrivate)) {
- dispatch(expandGroupMediaTimeline(group.id))
- // @ts-ignore
- .then(() => setLoading(false))
- .catch(() => {});
- }
- }, [group?.id, isMember, isPrivate]);
-
- const renderAttachments = () => {
- const nineAttachments = attachments.slice(0, 9);
-
- if (!nineAttachments.isEmpty()) {
- return (
-
- {nineAttachments.map((attachment, _index) => (
-
- ))}
-
- );
- } else {
- return (
-
-
-
- );
- }
- };
-
- if ((isPrivate && !isMember) || group?.deleted_at) {
- return null;
- }
-
- return (
- }>
- {group && (
-
- {loading ? (
-
- ) : (
- renderAttachments()
- )}
-
- )}
-
- );
-};
-
-export default GroupMediaPanel;
diff --git a/app/soapbox/features/ui/components/modals/__tests__/landing-page-modal.test.tsx b/app/soapbox/features/ui/components/modals/__tests__/landing-page-modal.test.tsx
deleted file mode 100644
index aa99a7f65..000000000
--- a/app/soapbox/features/ui/components/modals/__tests__/landing-page-modal.test.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react';
-
-import { storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
-import { render, screen } from 'soapbox/jest/test-helpers';
-
-import LandingPageModal from '../landing-page-modal';
-
-describe('', () => {
- it('successfully renders', () => {
- render();
- expect(screen.getByTestId('modal')).toBeInTheDocument();
- });
-
- it('doesn\'t display the signup button by default', () => {
- render();
- expect(screen.queryByText('Register')).not.toBeInTheDocument();
- });
-
- describe('with registrations enabled', () => {
- it('displays the signup button', () => {
- render(, undefined, storeOpen);
- expect(screen.getByText('Register')).toBeInTheDocument();
- });
- });
-
- describe('with registrations closed, Pepe enabled', () => {
- it('displays the signup button', () => {
- render(, undefined, storePepeOpen);
- expect(screen.getByText('Register')).toBeInTheDocument();
- });
- });
-});
diff --git a/app/soapbox/features/ui/components/modals/__tests__/unauthorized-modal.test.tsx b/app/soapbox/features/ui/components/modals/__tests__/unauthorized-modal.test.tsx
deleted file mode 100644
index db5f775b4..000000000
--- a/app/soapbox/features/ui/components/modals/__tests__/unauthorized-modal.test.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react';
-
-import { storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
-import { render, screen } from 'soapbox/jest/test-helpers';
-
-import UnauthorizedModal from '../unauthorized-modal';
-
-describe('', () => {
- it('successfully renders', () => {
- render();
- expect(screen.getByTestId('modal')).toBeInTheDocument();
- });
-
- it('doesn\'t display the signup button by default', () => {
- render();
- expect(screen.queryByText('Sign up')).not.toBeInTheDocument();
- });
-
- describe('with registrations enabled', () => {
- it('displays the signup button', () => {
- render(, undefined, storeOpen);
- expect(screen.getByText('Sign up')).toBeInTheDocument();
- });
- });
-
- describe('with registrations closed, Pepe enabled', () => {
- it('displays the signup button', () => {
- render(, undefined, storePepeOpen);
- expect(screen.getByText('Sign up')).toBeInTheDocument();
- });
- });
-});
diff --git a/app/soapbox/features/ui/components/modals/dislikes-modal.tsx b/app/soapbox/features/ui/components/modals/dislikes-modal.tsx
deleted file mode 100644
index 1522bcc81..000000000
--- a/app/soapbox/features/ui/components/modals/dislikes-modal.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import React, { useEffect } from 'react';
-import { FormattedMessage } from 'react-intl';
-
-import { fetchDislikes } from 'soapbox/actions/interactions';
-import ScrollableList from 'soapbox/components/scrollable-list';
-import { Modal, Spinner } from 'soapbox/components/ui';
-import AccountContainer from 'soapbox/containers/account-container';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-
-interface IDislikesModal {
- onClose: (type: string) => void
- statusId: string
-}
-
-const DislikesModal: React.FC = ({ onClose, statusId }) => {
- const dispatch = useAppDispatch();
-
- const accountIds = useAppSelector((state) => state.user_lists.disliked_by.get(statusId)?.items);
-
- const fetchData = () => {
- dispatch(fetchDislikes(statusId));
- };
-
- useEffect(() => {
- fetchData();
- }, []);
-
- const onClickClose = () => {
- onClose('DISLIKES');
- };
-
- let body;
-
- if (!accountIds) {
- body = ;
- } else {
- const emptyMessage = ;
-
- body = (
-
- {accountIds.map(id =>
- ,
- )}
-
- );
- }
-
- return (
- }
- onClose={onClickClose}
- >
- {body}
-
- );
-};
-
-export default DislikesModal;
diff --git a/app/soapbox/features/ui/components/modals/edit-announcement-modal.tsx b/app/soapbox/features/ui/components/modals/edit-announcement-modal.tsx
deleted file mode 100644
index 3b8c7736f..000000000
--- a/app/soapbox/features/ui/components/modals/edit-announcement-modal.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import React from 'react';
-import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
-
-import { changeAnnouncementAllDay, changeAnnouncementContent, changeAnnouncementEndTime, changeAnnouncementStartTime, handleCreateAnnouncement } from 'soapbox/actions/admin';
-import { closeModal } from 'soapbox/actions/modals';
-import { Form, FormGroup, HStack, Modal, Stack, Text, Textarea, Toggle } from 'soapbox/components/ui';
-import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
-import { DatePicker } from 'soapbox/features/ui/util/async-components';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-
-const messages = defineMessages({
- save: { id: 'admin.edit_announcement.save', defaultMessage: 'Save' },
- announcementContentPlaceholder: { id: 'admin.edit_announcement.fields.content_placeholder', defaultMessage: 'Announcement content' },
- announcementStartTimePlaceholder: { id: 'admin.edit_announcement.fields.start_time_placeholder', defaultMessage: 'Announcement starts on…' },
- announcementEndTimePlaceholder: { id: 'admin.edit_announcement.fields.end_time_placeholder', defaultMessage: 'Announcement ends on…' },
-});
-
-interface IEditAnnouncementModal {
- onClose: (type?: string) => void
-}
-
-const EditAnnouncementModal: React.FC = ({ onClose }) => {
- const dispatch = useAppDispatch();
- const intl = useIntl();
-
- const id = useAppSelector((state) => state.admin_announcements.form.id);
- const content = useAppSelector((state) => state.admin_announcements.form.content);
- const startTime = useAppSelector((state) => state.admin_announcements.form.starts_at);
- const endTime = useAppSelector((state) => state.admin_announcements.form.ends_at);
- const allDay = useAppSelector((state) => state.admin_announcements.form.all_day);
-
- const onChangeContent: React.ChangeEventHandler = ({ target }) =>
- dispatch(changeAnnouncementContent(target.value));
-
- const onChangeStartTime = (date: Date | null) => dispatch(changeAnnouncementStartTime(date));
-
- const onChangeEndTime = (date: Date | null) => dispatch(changeAnnouncementEndTime(date));
-
- const onChangeAllDay: React.ChangeEventHandler = ({ target }) => dispatch(changeAnnouncementAllDay(target.checked));
-
- const onClickClose = () => {
- onClose('EDIT_ANNOUNCEMENT');
- };
-
- const handleSubmit = () => dispatch(handleCreateAnnouncement()).then(() => dispatch(closeModal('EDIT_ANNOUNCEMENT')));
-
- return (
-
- : }
- confirmationAction={handleSubmit}
- confirmationText={intl.formatMessage(messages.save)}
- >
-