Merge remote-tracking branch 'origin/develop' into chats
This commit is contained in:
@@ -34,22 +34,23 @@ const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, remo
|
||||
<Text theme='muted'>
|
||||
<FormattedDate
|
||||
value={startsAt}
|
||||
hour12={false}
|
||||
hour12
|
||||
year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'}
|
||||
month='short'
|
||||
day='2-digit'
|
||||
hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'}
|
||||
hour={skipTime ? undefined : 'numeric'}
|
||||
minute={skipTime ? undefined : '2-digit'}
|
||||
/>
|
||||
{' '}
|
||||
-
|
||||
{' '}
|
||||
<FormattedDate
|
||||
value={endsAt}
|
||||
hour12={false}
|
||||
hour12
|
||||
year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'}
|
||||
month={skipEndDate ? undefined : 'short'}
|
||||
day={skipEndDate ? undefined : '2-digit'}
|
||||
hour={skipTime ? undefined : '2-digit'}
|
||||
hour={skipTime ? undefined : 'numeric'}
|
||||
minute={skipTime ? undefined : '2-digit'}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
@@ -6,9 +6,10 @@ import Bundle from 'soapbox/features/ui/components/bundle';
|
||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { Attachment } from 'soapbox/types/entities';
|
||||
|
||||
interface IAttachmentThumbs {
|
||||
media: ImmutableList<Immutable.Record<any>>
|
||||
media: ImmutableList<Attachment>
|
||||
onClick?(): void
|
||||
sensitive?: boolean
|
||||
}
|
||||
@@ -18,7 +19,7 @@ const AttachmentThumbs = (props: IAttachmentThumbs) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const renderLoading = () => <div className='media-gallery--compact' />;
|
||||
const onOpenMedia = (media: Immutable.Record<any>, index: number) => dispatch(openModal('MEDIA', { media, index }));
|
||||
const onOpenMedia = (media: ImmutableList<Attachment>, index: number) => dispatch(openModal('MEDIA', { media, index }));
|
||||
|
||||
return (
|
||||
<div className='attachment-thumbs'>
|
||||
@@ -30,6 +31,7 @@ const AttachmentThumbs = (props: IAttachmentThumbs) => {
|
||||
height={50}
|
||||
compact
|
||||
sensitive={sensitive}
|
||||
visible
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
|
||||
@@ -263,14 +263,13 @@ const Item: React.FC<IItem> = ({
|
||||
interface IMediaGallery {
|
||||
sensitive?: boolean,
|
||||
media: ImmutableList<Attachment>,
|
||||
size: number,
|
||||
height: number,
|
||||
onOpenMedia: (media: ImmutableList<Attachment>, index: number) => void,
|
||||
defaultWidth: number,
|
||||
cacheWidth: (width: number) => void,
|
||||
defaultWidth?: number,
|
||||
cacheWidth?: (width: number) => void,
|
||||
visible?: boolean,
|
||||
onToggleVisibility?: () => void,
|
||||
displayMedia: string,
|
||||
displayMedia?: string,
|
||||
compact: boolean,
|
||||
}
|
||||
|
||||
@@ -278,7 +277,7 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
|
||||
const {
|
||||
media,
|
||||
sensitive = false,
|
||||
defaultWidth,
|
||||
defaultWidth = 0,
|
||||
onToggleVisibility,
|
||||
onOpenMedia,
|
||||
cacheWidth,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
@@ -15,9 +15,10 @@ import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import { UserPanel } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import { isLocal } from 'soapbox/utils/accounts';
|
||||
|
||||
import { showProfileHoverCard } from './hover_ref_wrapper';
|
||||
import { Card, CardBody, Stack, Text } from './ui';
|
||||
import { Card, CardBody, HStack, Icon, Stack, Text } from './ui';
|
||||
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
@@ -60,6 +61,7 @@ interface IProfileHoverCard {
|
||||
export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
|
||||
|
||||
@@ -88,6 +90,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
||||
|
||||
if (!account) return null;
|
||||
const accountBio = { __html: account.note_emojified };
|
||||
const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' });
|
||||
const followedBy = me !== account.id && account.relationship?.followed_by === true;
|
||||
|
||||
return (
|
||||
@@ -116,6 +119,23 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
||||
)}
|
||||
</BundleContainer>
|
||||
|
||||
{isLocal(account) ? (
|
||||
<HStack alignItems='center' space={0.5}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/calendar.svg')}
|
||||
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
||||
/>
|
||||
|
||||
<Text size='sm'>
|
||||
<FormattedMessage
|
||||
id='account.member_since' defaultMessage='Joined {date}' values={{
|
||||
date: memberSinceDate,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
</HStack>
|
||||
) : null}
|
||||
|
||||
{account.source.get('note', '').length > 0 && (
|
||||
<Text size='sm' dangerouslySetInnerHTML={accountBio} />
|
||||
)}
|
||||
|
||||
@@ -17,11 +17,11 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const dateFormatOptions: FormatDateOptions = {
|
||||
hour12: false,
|
||||
hour12: true,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
@@ -32,8 +32,8 @@ const shortDateFormatOptions: FormatDateOptions = {
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 1000 * 60;
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
const DAY = 1000 * 60 * 60 * 24;
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
const DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
const MAX_DELAY = 2147483647;
|
||||
|
||||
@@ -170,12 +170,12 @@ class RelativeTimestamp extends React.Component<RelativeTimestampProps, Relative
|
||||
clearTimeout(this._timer);
|
||||
}
|
||||
|
||||
const { timestamp } = this.props;
|
||||
const delta = (new Date(timestamp)).getTime() - this.state.now;
|
||||
const unitDelay = getUnitDelay(selectUnits(delta));
|
||||
const unitRemainder = Math.abs(delta % unitDelay);
|
||||
const { timestamp } = this.props;
|
||||
const delta = (new Date(timestamp)).getTime() - this.state.now;
|
||||
const unitDelay = getUnitDelay(selectUnits(delta));
|
||||
const unitRemainder = Math.abs(delta % unitDelay);
|
||||
const updateInterval = 1000 * 10;
|
||||
const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
|
||||
const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
|
||||
|
||||
this._timer = setTimeout(() => {
|
||||
this.setState({ now: Date.now() });
|
||||
|
||||
@@ -374,7 +374,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.pinned ? messages.unpin : messages.pin),
|
||||
action: handlePinClick,
|
||||
icon: mutingConversation ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg'),
|
||||
icon: status.pinned ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg'),
|
||||
});
|
||||
} else {
|
||||
if (status.visibility === 'private') {
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
import classNames from 'clsx';
|
||||
|
||||
type ButtonThemes = 'primary' | 'secondary' | 'tertiary' | 'accent' | 'danger' | 'transparent' | 'outline'
|
||||
type ButtonSizes = 'sm' | 'md' | 'lg'
|
||||
const themes = {
|
||||
primary:
|
||||
'bg-primary-500 hover:bg-primary-400 dark:hover:bg-primary-600 border-transparent focus:bg-primary-500 text-gray-100 focus:ring-primary-300',
|
||||
secondary:
|
||||
'border-transparent bg-primary-100 dark:bg-primary-800 hover:bg-primary-50 dark:hover:bg-primary-700 focus:bg-primary-100 dark:focus:bg-primary-800 text-primary-500 dark:text-primary-200',
|
||||
tertiary:
|
||||
'bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500',
|
||||
accent: 'border-transparent bg-secondary-500 hover:bg-secondary-400 focus:bg-secondary-500 text-gray-100 focus:ring-secondary-300',
|
||||
danger: 'border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:bg-danger-800 dark:focus:bg-danger-600',
|
||||
transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80',
|
||||
outline: 'border-gray-100 border-2 bg-transparent text-gray-100 hover:bg-white/10',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
xs: 'px-3 py-1 text-xs',
|
||||
sm: 'px-3 py-1.5 text-xs leading-4',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base',
|
||||
};
|
||||
|
||||
type ButtonSizes = keyof typeof sizes
|
||||
type ButtonThemes = keyof typeof themes
|
||||
|
||||
type IButtonStyles = {
|
||||
theme: ButtonThemes,
|
||||
block: boolean,
|
||||
disabled: boolean,
|
||||
theme: ButtonThemes
|
||||
block: boolean
|
||||
disabled: boolean
|
||||
size: ButtonSizes
|
||||
}
|
||||
|
||||
@@ -17,26 +37,6 @@ const useButtonStyles = ({
|
||||
disabled,
|
||||
size,
|
||||
}: IButtonStyles) => {
|
||||
const themes = {
|
||||
primary:
|
||||
'bg-primary-500 hover:bg-primary-400 dark:hover:bg-primary-600 border-transparent focus:bg-primary-500 text-gray-100 focus:ring-primary-300',
|
||||
secondary:
|
||||
'border-transparent bg-primary-100 dark:bg-primary-800 hover:bg-primary-50 dark:hover:bg-primary-700 focus:bg-primary-100 dark:focus:bg-primary-800 text-primary-500 dark:text-primary-200',
|
||||
tertiary:
|
||||
'bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500',
|
||||
accent: 'border-transparent bg-secondary-500 hover:bg-secondary-400 focus:bg-secondary-500 text-gray-100 focus:ring-secondary-300',
|
||||
danger: 'border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:bg-danger-800 dark:focus:bg-danger-600',
|
||||
transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80',
|
||||
outline: 'border-gray-100 border-2 bg-transparent text-gray-100 hover:bg-white/10',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
xs: 'px-3 py-1 text-xs',
|
||||
sm: 'px-3 py-1.5 text-xs leading-4',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base',
|
||||
};
|
||||
|
||||
const buttonStyle = classNames({
|
||||
'inline-flex items-center border font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 appearance-none transition-all': true,
|
||||
'select-none disabled:opacity-75 disabled:cursor-default': disabled,
|
||||
|
||||
@@ -18,13 +18,13 @@ const messages = defineMessages({
|
||||
|
||||
interface ICard {
|
||||
/** The type of card. */
|
||||
variant?: 'default' | 'rounded',
|
||||
variant?: 'default' | 'rounded'
|
||||
/** Card size preset. */
|
||||
size?: 'md' | 'lg' | 'xl',
|
||||
size?: keyof typeof sizes
|
||||
/** Extra classnames for the <div> element. */
|
||||
className?: string,
|
||||
className?: string
|
||||
/** Elements inside the card. */
|
||||
children: React.ReactNode,
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** An opaque backdrop to hold a collection of related elements. */
|
||||
|
||||
@@ -17,7 +17,7 @@ const alignItemsOptions = {
|
||||
};
|
||||
|
||||
const spaces = {
|
||||
'0.5': 'space-x-0.5',
|
||||
[0.5]: 'space-x-0.5',
|
||||
1: 'space-x-1',
|
||||
1.5: 'space-x-1.5',
|
||||
2: 'space-x-2',
|
||||
@@ -29,21 +29,21 @@ const spaces = {
|
||||
|
||||
interface IHStack {
|
||||
/** Vertical alignment of children. */
|
||||
alignItems?: 'top' | 'bottom' | 'center' | 'start',
|
||||
alignItems?: keyof typeof alignItemsOptions
|
||||
/** Extra class names on the <div> element. */
|
||||
className?: string,
|
||||
className?: string
|
||||
/** Children */
|
||||
children?: React.ReactNode,
|
||||
children?: React.ReactNode
|
||||
/** Horizontal alignment of children. */
|
||||
justifyContent?: 'between' | 'center' | 'start' | 'end' | 'around',
|
||||
justifyContent?: keyof typeof justifyContentOptions
|
||||
/** Size of the gap between elements. */
|
||||
space?: 0.5 | 1 | 1.5 | 2 | 3 | 4 | 6 | 8,
|
||||
space?: keyof typeof spaces
|
||||
/** Whether to let the flexbox grow. */
|
||||
grow?: boolean,
|
||||
grow?: boolean
|
||||
/** Extra CSS styles for the <div> */
|
||||
style?: React.CSSProperties
|
||||
/** Whether to let the flexbox wrap onto multiple lines. */
|
||||
wrap?: boolean,
|
||||
wrap?: boolean
|
||||
}
|
||||
|
||||
/** Horizontal row of child elements. */
|
||||
|
||||
@@ -11,8 +11,6 @@ const messages = defineMessages({
|
||||
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
type Widths = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
|
||||
|
||||
const widths = {
|
||||
xs: 'max-w-xs',
|
||||
sm: 'max-w-sm',
|
||||
@@ -52,7 +50,7 @@ interface IModal {
|
||||
skipFocus?: boolean,
|
||||
/** Title text for the modal. */
|
||||
title?: React.ReactNode,
|
||||
width?: Widths,
|
||||
width?: keyof typeof widths,
|
||||
}
|
||||
|
||||
/** Displays a modal dialog box. */
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
type SIZES = 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 10
|
||||
|
||||
const spaces = {
|
||||
0: 'space-y-0',
|
||||
'0.5': 'space-y-0.5',
|
||||
[0.5]: 'space-y-0.5',
|
||||
1: 'space-y-1',
|
||||
'1.5': 'space-y-1.5',
|
||||
[1.5]: 'space-y-1.5',
|
||||
2: 'space-y-2',
|
||||
3: 'space-y-3',
|
||||
4: 'space-y-4',
|
||||
@@ -27,15 +25,15 @@ const alignItemsOptions = {
|
||||
|
||||
interface IStack extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Size of the gap between elements. */
|
||||
space?: SIZES,
|
||||
space?: keyof typeof spaces
|
||||
/** Horizontal alignment of children. */
|
||||
alignItems?: 'center' | 'start',
|
||||
/** Vertical alignment of children. */
|
||||
justifyContent?: 'center',
|
||||
justifyContent?: 'center'
|
||||
/** Extra class names on the <div> element. */
|
||||
className?: string,
|
||||
className?: string
|
||||
/** Whether to let the flexbox grow. */
|
||||
grow?: boolean,
|
||||
grow?: boolean
|
||||
}
|
||||
|
||||
/** Vertical stack of child elements. */
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
[data-reach-tab] {
|
||||
@apply flex-1 flex justify-center items-center
|
||||
py-4 px-1 text-center font-medium text-sm text-gray-700
|
||||
dark:text-gray-600 hover:text-gray-800 dark:hover:text-gray-500
|
||||
focus:ring-primary-300 focus:ring-2;
|
||||
dark:text-gray-600 hover:text-gray-800 dark:hover:text-gray-500;
|
||||
}
|
||||
|
||||
[data-reach-tab][data-selected] {
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
type Themes = 'default' | 'danger' | 'primary' | 'muted' | 'subtle' | 'success' | 'inherit' | 'white'
|
||||
type Weights = 'normal' | 'medium' | 'semibold' | 'bold'
|
||||
export type Sizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'
|
||||
type Alignments = 'left' | 'center' | 'right'
|
||||
type TrackingSizes = 'normal' | 'wide'
|
||||
type TransformProperties = 'uppercase' | 'normal'
|
||||
type Families = 'sans' | 'mono'
|
||||
type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label'
|
||||
type Directions = 'ltr' | 'rtl'
|
||||
|
||||
const themes = {
|
||||
default: 'text-gray-900 dark:text-gray-100',
|
||||
danger: 'text-danger-600',
|
||||
@@ -60,15 +50,19 @@ const families = {
|
||||
mono: 'font-mono',
|
||||
};
|
||||
|
||||
export type Sizes = keyof typeof sizes
|
||||
type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label'
|
||||
type Directions = 'ltr' | 'rtl'
|
||||
|
||||
interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'dangerouslySetInnerHTML'> {
|
||||
/** How to align the text. */
|
||||
align?: Alignments,
|
||||
align?: keyof typeof alignments,
|
||||
/** Extra class names for the outer element. */
|
||||
className?: string,
|
||||
/** Text direction. */
|
||||
direction?: Directions,
|
||||
/** Typeface of the text. */
|
||||
family?: Families,
|
||||
family?: keyof typeof families,
|
||||
/** The "for" attribute specifies which form element a label is bound to. */
|
||||
htmlFor?: string,
|
||||
/** Font size of the text. */
|
||||
@@ -76,15 +70,15 @@ interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'danger
|
||||
/** HTML element name of the outer element. */
|
||||
tag?: Tags,
|
||||
/** Theme for the text. */
|
||||
theme?: Themes,
|
||||
theme?: keyof typeof themes,
|
||||
/** Letter-spacing of the text. */
|
||||
tracking?: TrackingSizes,
|
||||
tracking?: keyof typeof trackingSizes,
|
||||
/** Transform (eg uppercase) for the text. */
|
||||
transform?: TransformProperties,
|
||||
transform?: keyof typeof transformProperties,
|
||||
/** Whether to truncate the text if its container is too small. */
|
||||
truncate?: boolean,
|
||||
/** Font weight of the text. */
|
||||
weight?: Weights,
|
||||
weight?: keyof typeof weights,
|
||||
/** Tooltip title. */
|
||||
title?: string,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user