Merge commit '32f0e619f0
' into kb_migration_development
This commit is contained in:
commit
7c118ed1d0
446 changed files with 6295 additions and 3456 deletions
|
@ -1,17 +0,0 @@
|
|||
export const APP_FOCUS = 'APP_FOCUS';
|
||||
export const APP_UNFOCUS = 'APP_UNFOCUS';
|
||||
|
||||
export const focusApp = () => ({
|
||||
type: APP_FOCUS,
|
||||
});
|
||||
|
||||
export const unfocusApp = () => ({
|
||||
type: APP_UNFOCUS,
|
||||
});
|
||||
|
||||
export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE';
|
||||
|
||||
export const changeLayout = layout => ({
|
||||
type: APP_LAYOUT_CHANGE,
|
||||
layout,
|
||||
});
|
10
app/javascript/mastodon/actions/app.ts
Normal file
10
app/javascript/mastodon/actions/app.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const focusApp = createAction('APP_FOCUS');
|
||||
export const unfocusApp = createAction('APP_UNFOCUS');
|
||||
|
||||
type ChangeLayoutPayload = {
|
||||
layout: 'mobile' | 'single-column' | 'multi-column';
|
||||
};
|
||||
export const changeLayout =
|
||||
createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');
|
|
@ -84,7 +84,7 @@ const DIGIT_CHARACTERS = [
|
|||
'~',
|
||||
];
|
||||
|
||||
export const decode83 = (str) => {
|
||||
export const decode83 = (str: string) => {
|
||||
let value = 0;
|
||||
let c, digit;
|
||||
|
||||
|
@ -97,13 +97,13 @@ export const decode83 = (str) => {
|
|||
return value;
|
||||
};
|
||||
|
||||
export const intToRGB = int => ({
|
||||
export const intToRGB = (int: number) => ({
|
||||
r: Math.max(0, (int >> 16)),
|
||||
g: Math.max(0, (int >> 8) & 255),
|
||||
b: Math.max(0, (int & 255)),
|
||||
});
|
||||
|
||||
export const getAverageFromBlurhash = blurhash => {
|
||||
export const getAverageFromBlurhash = (blurhash: string) => {
|
||||
if (!blurhash) {
|
||||
return null;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export default function compareId (id1, id2) {
|
||||
export default function compareId (id1: string, id2: string) {
|
||||
if (id1 === id2) {
|
||||
return 0;
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
import { decode } from 'blurhash';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* @typedef BlurhashPropsBase
|
||||
* @property {string?} hash Hash to render
|
||||
* @property {number} width
|
||||
* Width of the blurred region in pixels. Defaults to 32
|
||||
* @property {number} [height]
|
||||
* Height of the blurred region in pixels. Defaults to width
|
||||
* @property {boolean} [dummy]
|
||||
* Whether dummy mode is enabled. If enabled, nothing is rendered
|
||||
* and canvas left untouched
|
||||
*/
|
||||
|
||||
/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */
|
||||
|
||||
/**
|
||||
* Component that is used to render blurred of blurhash string
|
||||
* @param {BlurhashProps} param1 Props of the component
|
||||
* @returns {JSX.Element} Canvas which will render blurred region element to embed
|
||||
*/
|
||||
function Blurhash({
|
||||
hash,
|
||||
width = 32,
|
||||
height = width,
|
||||
dummy = false,
|
||||
...canvasProps
|
||||
}) {
|
||||
const canvasRef = /** @type {import('react').MutableRefObject<HTMLCanvasElement>} */ (useRef());
|
||||
|
||||
useEffect(() => {
|
||||
const { current: canvas } = canvasRef;
|
||||
canvas.width = canvas.width; // resets canvas
|
||||
|
||||
if (dummy || !hash) return;
|
||||
|
||||
try {
|
||||
const pixels = decode(hash, width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, width, height);
|
||||
|
||||
// @ts-expect-error
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
} catch (err) {
|
||||
console.error('Blurhash decoding failure', { err, hash });
|
||||
}
|
||||
}, [dummy, hash, width, height]);
|
||||
|
||||
return (
|
||||
<canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
|
||||
);
|
||||
}
|
||||
|
||||
Blurhash.propTypes = {
|
||||
hash: PropTypes.string.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
dummy: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default React.memo(Blurhash);
|
45
app/javascript/mastodon/components/blurhash.tsx
Normal file
45
app/javascript/mastodon/components/blurhash.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { decode } from 'blurhash';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
|
||||
type Props = {
|
||||
hash: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
|
||||
children?: never;
|
||||
[key: string]: any;
|
||||
}
|
||||
function Blurhash({
|
||||
hash,
|
||||
width = 32,
|
||||
height = width,
|
||||
dummy = false,
|
||||
...canvasProps
|
||||
}: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const canvas = canvasRef.current!;
|
||||
// eslint-disable-next-line no-self-assign
|
||||
canvas.width = canvas.width; // resets canvas
|
||||
|
||||
if (dummy || !hash) return;
|
||||
|
||||
try {
|
||||
const pixels = decode(hash, width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, width, height);
|
||||
|
||||
ctx?.putImageData(imageData, 0, 0);
|
||||
} catch (err) {
|
||||
console.error('Blurhash decoding failure', { err, hash });
|
||||
}
|
||||
}, [dummy, hash, width, height]);
|
||||
|
||||
return (
|
||||
<canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Blurhash);
|
|
@ -21,7 +21,9 @@ export default class ColumnBackButton extends React.PureComponent {
|
|||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
} else if (window.history && window.history.state) {
|
||||
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
|
||||
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
|
||||
} else if (router.route.location.key) {
|
||||
router.history.goBack();
|
||||
} else {
|
||||
router.history.push('/');
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import AnimatedNumber from 'mastodon/components/animated_number';
|
||||
import { Icon } from './icon';
|
||||
import { AnimatedNumber } from './animated_number';
|
||||
|
||||
export default class IconButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onKeyPress: PropTypes.func,
|
||||
size: PropTypes.number,
|
||||
active: PropTypes.bool,
|
||||
expanded: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
activeStyle: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
inverted: PropTypes.bool,
|
||||
animate: PropTypes.bool,
|
||||
overlay: PropTypes.bool,
|
||||
tabIndex: PropTypes.number,
|
||||
counter: PropTypes.number,
|
||||
obfuscateCount: PropTypes.bool,
|
||||
href: PropTypes.string,
|
||||
ariaHidden: PropTypes.bool,
|
||||
};
|
||||
type Props = {
|
||||
className?: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
|
||||
onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
|
||||
size: number;
|
||||
active: boolean;
|
||||
expanded?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
activeStyle?: React.CSSProperties;
|
||||
disabled: boolean;
|
||||
inverted?: boolean;
|
||||
animate: boolean;
|
||||
overlay: boolean;
|
||||
tabIndex: number;
|
||||
counter?: number;
|
||||
obfuscateCount?: boolean;
|
||||
href?: string;
|
||||
ariaHidden: boolean;
|
||||
}
|
||||
type States = {
|
||||
activate: boolean,
|
||||
deactivate: boolean,
|
||||
}
|
||||
export default class IconButton extends React.PureComponent<Props, States> {
|
||||
|
||||
static defaultProps = {
|
||||
size: 18,
|
||||
|
@ -45,7 +47,7 @@ export default class IconButton extends React.PureComponent {
|
|||
deactivate: false,
|
||||
};
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
UNSAFE_componentWillReceiveProps (nextProps: Props) {
|
||||
if (!nextProps.animate) return;
|
||||
|
||||
if (this.props.active && !nextProps.active) {
|
||||
|
@ -55,27 +57,27 @@ export default class IconButton extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.props.disabled) {
|
||||
if (!this.props.disabled && this.props.onClick != null) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (this.props.onKeyPress && !this.props.disabled) {
|
||||
this.props.onKeyPress(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseDown = (e) => {
|
||||
handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (!this.props.disabled && this.props.onMouseDown) {
|
||||
this.props.onMouseDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (!this.props.disabled && this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
|
@ -132,7 +134,7 @@ export default class IconButton extends React.PureComponent {
|
|||
</React.Fragment>
|
||||
);
|
||||
|
||||
if (href && !this.prop) {
|
||||
if (href != null) {
|
||||
contents = (
|
||||
<a href={href} target='_blank' rel='noopener noreferrer'>
|
||||
{contents}
|
|
@ -81,12 +81,10 @@ class Item extends React.PureComponent {
|
|||
render () {
|
||||
const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
|
||||
|
||||
let badges = [], thumbnail;
|
||||
|
||||
let width = 50;
|
||||
let height = 100;
|
||||
let top = 'auto';
|
||||
let left = 'auto';
|
||||
let bottom = 'auto';
|
||||
let right = 'auto';
|
||||
|
||||
if (size === 1) {
|
||||
width = 100;
|
||||
|
@ -106,60 +104,13 @@ class Item extends React.PureComponent {
|
|||
width = 100;
|
||||
}
|
||||
|
||||
if (size === 2) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
} else {
|
||||
left = '2px';
|
||||
}
|
||||
} else if (size === 3) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
} else if (index > 0) {
|
||||
left = '2px';
|
||||
}
|
||||
|
||||
if (index === 1) {
|
||||
bottom = '2px';
|
||||
} else if (index > 1) {
|
||||
top = '2px';
|
||||
}
|
||||
} else if (size === 4) {
|
||||
if (index === 0 || index === 2) {
|
||||
right = '2px';
|
||||
}
|
||||
|
||||
if (index === 1 || index === 3) {
|
||||
left = '2px';
|
||||
}
|
||||
|
||||
if (index < 2) {
|
||||
bottom = '2px';
|
||||
} else {
|
||||
top = '2px';
|
||||
}
|
||||
} else {
|
||||
if (index % 2 === 0) {
|
||||
right = '2px';
|
||||
}
|
||||
|
||||
if (index % 2 === 1) {
|
||||
left = '2px';
|
||||
}
|
||||
|
||||
if (index >= 2) {
|
||||
top = '2px';
|
||||
}
|
||||
if (index < size - 1) {
|
||||
bottom = '2px';
|
||||
}
|
||||
if (attachment.get('description')?.length > 0) {
|
||||
badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
|
||||
}
|
||||
|
||||
let thumbnail = '';
|
||||
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
|
||||
<Blurhash
|
||||
hash={attachment.get('blurhash')}
|
||||
|
@ -209,6 +160,8 @@ class Item extends React.PureComponent {
|
|||
} else if (attachment.get('type') === 'gifv') {
|
||||
const autoPlay = this.getAutoPlay();
|
||||
|
||||
badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>);
|
||||
|
||||
thumbnail = (
|
||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||
<video
|
||||
|
@ -226,14 +179,12 @@ class Item extends React.PureComponent {
|
|||
loop
|
||||
muted
|
||||
/>
|
||||
|
||||
<span className='media-gallery__gifv__label'>GIF</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||
<Blurhash
|
||||
hash={attachment.get('blurhash')}
|
||||
dummy={!useBlurhash}
|
||||
|
@ -241,7 +192,14 @@ class Item extends React.PureComponent {
|
|||
'media-gallery__preview--hidden': visible && this.state.loaded,
|
||||
})}
|
||||
/>
|
||||
|
||||
{visible && thumbnail}
|
||||
|
||||
{badges && (
|
||||
<div className='media-gallery__item__badges'>
|
||||
{badges}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -338,7 +296,7 @@ class MediaGallery extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { media, lang, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
|
||||
const { media, lang, intl, sensitive, defaultWidth, standalone, autoplay } = this.props;
|
||||
const { visible } = this.state;
|
||||
const width = this.state.width || defaultWidth;
|
||||
|
||||
|
@ -347,13 +305,9 @@ class MediaGallery extends React.PureComponent {
|
|||
const style = {};
|
||||
|
||||
if (this.isFullSizeEligible() && (standalone || !cropImages)) {
|
||||
if (width) {
|
||||
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
}
|
||||
} else if (width) {
|
||||
style.height = width / (16/9);
|
||||
style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
|
||||
} else {
|
||||
style.height = height;
|
||||
style.aspectRatio = '16 / 9';
|
||||
}
|
||||
|
||||
const maxSize = displayMediaExpand ? 8 : 4;
|
||||
|
|
|
@ -3,62 +3,22 @@ import PropTypes from 'prop-types';
|
|||
import Icon from 'mastodon/components/icon';
|
||||
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
|
||||
import { connect } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
class PictureInPicturePlaceholder extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
width: PropTypes.number,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
width: this.props.width,
|
||||
height: this.props.width && (this.props.width / (16/9)),
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(removePictureInPicture());
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
|
||||
if (this.node) {
|
||||
this._setDimensions();
|
||||
}
|
||||
};
|
||||
|
||||
_setDimensions () {
|
||||
const width = this.node.offsetWidth;
|
||||
const height = width / (16/9);
|
||||
|
||||
this.setState({ width, height });
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
handleResize = debounce(() => {
|
||||
if (this.node) {
|
||||
this._setDimensions();
|
||||
}
|
||||
}, 250, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
render () {
|
||||
const { height } = this.state;
|
||||
|
||||
return (
|
||||
<div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex={0} onClick={this.handleClick}>
|
||||
<div className='picture-in-picture-placeholder' role='button' tabIndex={0} onClick={this.handleClick}>
|
||||
<Icon id='window-restore' />
|
||||
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
|
||||
</div>
|
||||
|
|
|
@ -416,7 +416,7 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (pictureInPicture.get('inUse')) {
|
||||
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
|
||||
media = <PictureInPicturePlaceholder />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (this.props.muted) {
|
||||
media = (
|
||||
|
@ -465,12 +465,9 @@ class Status extends ImmutablePureComponent {
|
|||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
lang={status.get('language')}
|
||||
width={this.props.cachedMediaWidth}
|
||||
height={110}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
|
@ -503,8 +500,6 @@ class Status extends ImmutablePureComponent {
|
|||
onOpenMedia={this.handleOpenMedia}
|
||||
card={status.get('card')}
|
||||
compact
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
sensitive={status.get('sensitive')}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import configureStore from '../store/configureStore';
|
||||
import { store } from '../store/configureStore';
|
||||
import { hydrateStore } from '../actions/store';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { getLocale } from '../locales';
|
||||
|
@ -12,8 +12,6 @@ import { fetchCustomEmojis } from '../actions/custom_emojis';
|
|||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
if (initialState) {
|
||||
store.dispatch(hydrateStore(initialState));
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
|
|||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { BrowserRouter, Route } from 'react-router-dom';
|
||||
import { ScrollContext } from 'react-router-scroll-4';
|
||||
import configureStore from 'mastodon/store/configureStore';
|
||||
import { store } from 'mastodon/store/configureStore';
|
||||
import UI from 'mastodon/features/ui';
|
||||
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
|
||||
import { hydrateStore } from 'mastodon/actions/store';
|
||||
|
@ -19,7 +19,6 @@ addLocaleData(localeData);
|
|||
|
||||
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
|
||||
|
||||
export const store = configureStore();
|
||||
const hydrateAction = hydrateStore(initialState);
|
||||
|
||||
store.dispatch(hydrateAction);
|
||||
|
|
|
@ -384,7 +384,7 @@ class Audio extends React.PureComponent {
|
|||
}
|
||||
|
||||
_getRadius () {
|
||||
return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2);
|
||||
return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient());
|
||||
}
|
||||
|
||||
_getScaleCoefficient () {
|
||||
|
@ -396,7 +396,7 @@ class Audio extends React.PureComponent {
|
|||
}
|
||||
|
||||
_getCY() {
|
||||
return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient()));
|
||||
return Math.floor((this.state.height || this.props.height) / 2);
|
||||
}
|
||||
|
||||
_getAccentColor () {
|
||||
|
@ -470,7 +470,7 @@ class Audio extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
|
||||
<div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
|
||||
|
||||
<Blurhash
|
||||
hash={blurhash}
|
||||
|
@ -515,9 +515,16 @@ class Audio extends React.PureComponent {
|
|||
{(revealed || editable) && <img
|
||||
src={this.props.poster}
|
||||
alt=''
|
||||
width={(this._getRadius() - TICK_SIZE) * 2}
|
||||
height={(this._getRadius() - TICK_SIZE) * 2}
|
||||
style={{ position: 'absolute', left: this._getCX(), top: this._getCY(), transform: 'translate(-50%, -50%)', borderRadius: '50%', pointerEvents: 'none' }}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`,
|
||||
aspectRatio: '1',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>}
|
||||
|
||||
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
|
||||
|
|
|
@ -8,7 +8,6 @@ import classnames from 'classnames';
|
|||
import Icon from 'mastodon/components/icon';
|
||||
import { useBlurhash } from 'mastodon/initial_state';
|
||||
import Blurhash from 'mastodon/components/blurhash';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const IDNA_PREFIX = 'xn--';
|
||||
|
||||
|
@ -54,8 +53,6 @@ export default class Card extends React.PureComponent {
|
|||
card: ImmutablePropTypes.map,
|
||||
onOpenMedia: PropTypes.func.isRequired,
|
||||
compact: PropTypes.bool,
|
||||
defaultWidth: PropTypes.number,
|
||||
cacheWidth: PropTypes.func,
|
||||
sensitive: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
@ -64,7 +61,6 @@ export default class Card extends React.PureComponent {
|
|||
};
|
||||
|
||||
state = {
|
||||
width: this.props.defaultWidth || 280,
|
||||
previewLoaded: false,
|
||||
embedded: false,
|
||||
revealed: !this.props.sensitive,
|
||||
|
@ -87,24 +83,6 @@ export default class Card extends React.PureComponent {
|
|||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
_setDimensions () {
|
||||
const width = this.node.offsetWidth;
|
||||
|
||||
if (this.props.cacheWidth) {
|
||||
this.props.cacheWidth(width);
|
||||
}
|
||||
|
||||
this.setState({ width });
|
||||
}
|
||||
|
||||
handleResize = debounce(() => {
|
||||
if (this.node) {
|
||||
this._setDimensions();
|
||||
}
|
||||
}, 250, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
handlePhotoClick = () => {
|
||||
const { card, onOpenMedia } = this.props;
|
||||
|
||||
|
@ -138,10 +116,6 @@ export default class Card extends React.PureComponent {
|
|||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
|
||||
if (this.node) {
|
||||
this._setDimensions();
|
||||
}
|
||||
};
|
||||
|
||||
handleImageLoad = () => {
|
||||
|
@ -157,36 +131,31 @@ export default class Card extends React.PureComponent {
|
|||
renderVideo () {
|
||||
const { card } = this.props;
|
||||
const content = { __html: addAutoPlay(card.get('html')) };
|
||||
const { width } = this.state;
|
||||
const ratio = card.get('width') / card.get('height');
|
||||
const height = width / ratio;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.setRef}
|
||||
className='status-card__image status-card-video'
|
||||
dangerouslySetInnerHTML={content}
|
||||
style={{ height }}
|
||||
style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { card, compact } = this.props;
|
||||
const { width, embedded, revealed } = this.state;
|
||||
const { embedded, revealed } = this.state;
|
||||
|
||||
if (card === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
|
||||
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
|
||||
const horizontal = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded;
|
||||
const interactive = card.get('type') !== 'link';
|
||||
const className = classnames('status-card', { horizontal, compact, interactive });
|
||||
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
||||
const language = card.get('language') || '';
|
||||
const ratio = card.get('width') / card.get('height');
|
||||
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
||||
|
||||
const description = (
|
||||
<div className='status-card__content' lang={language}>
|
||||
|
@ -196,6 +165,14 @@ export default class Card extends React.PureComponent {
|
|||
</div>
|
||||
);
|
||||
|
||||
const thumbnailStyle = {
|
||||
visibility: revealed? null : 'hidden',
|
||||
};
|
||||
|
||||
if (horizontal) {
|
||||
thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`;
|
||||
}
|
||||
|
||||
let embed = '';
|
||||
let canvas = (
|
||||
<Blurhash
|
||||
|
@ -206,7 +183,7 @@ export default class Card extends React.PureComponent {
|
|||
dummy={!useBlurhash}
|
||||
/>
|
||||
);
|
||||
let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
|
||||
let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
|
||||
let spoilerButton = (
|
||||
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
|
||||
<span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
||||
|
|
|
@ -368,7 +368,7 @@ class UI extends React.PureComponent {
|
|||
|
||||
if (layout !== this.props.layout) {
|
||||
this.handleLayoutChange.cancel();
|
||||
this.props.dispatch(changeLayout(layout));
|
||||
this.props.dispatch(changeLayout({ layout }));
|
||||
} else {
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { is } from 'immutable';
|
||||
import { throttle, debounce } from 'lodash';
|
||||
import { throttle } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
||||
import { displayMedia, useBlurhash } from '../../initial_state';
|
||||
|
@ -102,8 +102,6 @@ class Video extends React.PureComponent {
|
|||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string,
|
||||
lang: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
sensitive: PropTypes.bool,
|
||||
currentTime: PropTypes.number,
|
||||
onOpenVideo: PropTypes.func,
|
||||
|
@ -112,7 +110,6 @@ class Video extends React.PureComponent {
|
|||
inline: PropTypes.bool,
|
||||
editable: PropTypes.bool,
|
||||
alwaysVisible: PropTypes.bool,
|
||||
cacheWidth: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
onToggleVisibility: PropTypes.func,
|
||||
deployPictureInPicture: PropTypes.func,
|
||||
|
@ -135,7 +132,6 @@ class Video extends React.PureComponent {
|
|||
volume: 0.5,
|
||||
paused: true,
|
||||
dragging: false,
|
||||
containerWidth: this.props.width,
|
||||
fullscreen: false,
|
||||
hovered: false,
|
||||
muted: false,
|
||||
|
@ -144,24 +140,8 @@ class Video extends React.PureComponent {
|
|||
|
||||
setPlayerRef = c => {
|
||||
this.player = c;
|
||||
|
||||
if (this.player) {
|
||||
this._setDimensions();
|
||||
}
|
||||
};
|
||||
|
||||
_setDimensions () {
|
||||
const width = this.player.offsetWidth;
|
||||
|
||||
if (this.props.cacheWidth) {
|
||||
this.props.cacheWidth(width);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
containerWidth: width,
|
||||
});
|
||||
}
|
||||
|
||||
setVideoRef = c => {
|
||||
this.video = c;
|
||||
|
||||
|
@ -370,12 +350,10 @@ class Video extends React.PureComponent {
|
|||
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
|
||||
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
|
||||
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
|
||||
|
@ -404,14 +382,6 @@ class Video extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleResize = debounce(() => {
|
||||
if (this.player) {
|
||||
this._setDimensions();
|
||||
}
|
||||
}, 250, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
handleScroll = throttle(() => {
|
||||
if (!this.video) {
|
||||
return;
|
||||
|
@ -525,17 +495,12 @@ class Video extends React.PureComponent {
|
|||
|
||||
render () {
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const playerStyle = {};
|
||||
|
||||
let { width, height } = this.props;
|
||||
|
||||
if (inline && containerWidth) {
|
||||
width = containerWidth;
|
||||
height = containerWidth / (16/9);
|
||||
|
||||
playerStyle.height = height;
|
||||
if (inline) {
|
||||
playerStyle.aspectRatio = '16 / 9';
|
||||
}
|
||||
|
||||
let preload;
|
||||
|
@ -586,8 +551,6 @@ class Video extends React.PureComponent {
|
|||
aria-label={alt}
|
||||
title={alt}
|
||||
lang={lang}
|
||||
width={width}
|
||||
height={height}
|
||||
volume={volume}
|
||||
onClick={this.togglePlay}
|
||||
onKeyDown={this.handleVideoKeyDown}
|
||||
|
@ -596,6 +559,7 @@ class Video extends React.PureComponent {
|
|||
onLoadedData={this.handleLoadedData}
|
||||
onProgress={this.handleProgress}
|
||||
onVolumeChange={this.handleVolumeChange}
|
||||
style={{ ...playerStyle, width: '100%' }}
|
||||
/>}
|
||||
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
|
||||
|
|
|
@ -1,21 +1,12 @@
|
|||
// @ts-check
|
||||
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
// @ts-expect-error
|
||||
import { forceSingleColumn } from 'mastodon/initial_state';
|
||||
import { forceSingleColumn } from './initial_state';
|
||||
|
||||
const LAYOUT_BREAKPOINT = 630;
|
||||
|
||||
/**
|
||||
* @param {number} width
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isMobile = width => width <= LAYOUT_BREAKPOINT;
|
||||
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export const layoutFromWindow = () => {
|
||||
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
|
||||
export const layoutFromWindow = (): LayoutType => {
|
||||
if (isMobile(window.innerWidth)) {
|
||||
return 'mobile';
|
||||
} else if (forceSingleColumn) {
|
||||
|
@ -25,8 +16,9 @@ export const layoutFromWindow = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && window.MSStream != null;
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { setupBrowserNotifications } from 'mastodon/actions/notifications';
|
||||
import Mastodon, { store } from 'mastodon/containers/mastodon';
|
||||
import Mastodon from 'mastodon/containers/mastodon';
|
||||
import { store } from 'mastodon/store/configureStore';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import ready from 'mastodon/ready';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { STORE_HYDRATE } from 'mastodon/actions/store';
|
||||
import { APP_LAYOUT_CHANGE } from 'mastodon/actions/app';
|
||||
import { changeLayout } from 'mastodon/actions/app';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||
|
||||
|
@ -14,8 +14,8 @@ export default function meta(state = initialState, action) {
|
|||
switch(action.type) {
|
||||
case STORE_HYDRATE:
|
||||
return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions']));
|
||||
case APP_LAYOUT_CHANGE:
|
||||
return state.set('layout', action.layout);
|
||||
case changeLayout.type:
|
||||
return state.set('layout', action.payload.layout);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { NOTIFICATIONS_UPDATE } from 'mastodon/actions/notifications';
|
||||
import { APP_FOCUS, APP_UNFOCUS } from 'mastodon/actions/app';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
focused: true,
|
||||
unread: 0,
|
||||
});
|
||||
|
||||
export default function missed_updates(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case APP_FOCUS:
|
||||
return state.set('focused', true).set('unread', 0);
|
||||
case APP_UNFOCUS:
|
||||
return state.set('focused', false);
|
||||
case NOTIFICATIONS_UPDATE:
|
||||
return state.get('focused') ? state : state.update('unread', x => x + 1);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
31
app/javascript/mastodon/reducers/missed_updates.ts
Normal file
31
app/javascript/mastodon/reducers/missed_updates.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Record } from 'immutable';
|
||||
import type { Action } from 'redux';
|
||||
import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
|
||||
import { focusApp, unfocusApp } from '../actions/app';
|
||||
|
||||
type MissedUpdatesState = {
|
||||
focused: boolean;
|
||||
unread: number;
|
||||
};
|
||||
const initialState = Record<MissedUpdatesState>({
|
||||
focused: true,
|
||||
unread: 0,
|
||||
})();
|
||||
|
||||
export default function missed_updates(
|
||||
state = initialState,
|
||||
action: Action<string>,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case focusApp.type:
|
||||
return state.set('focused', true).set('unread', 0);
|
||||
case unfocusApp.type:
|
||||
return state.set('focused', false);
|
||||
case NOTIFICATIONS_UPDATE:
|
||||
return state.get('focused')
|
||||
? state
|
||||
: state.update('unread', (x) => x + 1);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -23,8 +23,8 @@ import {
|
|||
MARKERS_FETCH_SUCCESS,
|
||||
} from '../actions/markers';
|
||||
import {
|
||||
APP_FOCUS,
|
||||
APP_UNFOCUS,
|
||||
focusApp,
|
||||
unfocusApp,
|
||||
} from '../actions/app';
|
||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
||||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||
|
@ -259,9 +259,9 @@ export default function notifications(state = initialState, action) {
|
|||
return updateMounted(state);
|
||||
case NOTIFICATIONS_UNMOUNT:
|
||||
return state.update('mounted', count => count - 1);
|
||||
case APP_FOCUS:
|
||||
case focusApp.type:
|
||||
return updateVisibility(state, true);
|
||||
case APP_UNFOCUS:
|
||||
case unfocusApp.type:
|
||||
return updateVisibility(state, false);
|
||||
case NOTIFICATIONS_LOAD_PENDING:
|
||||
return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
|
||||
|
||||
const scroll = (node, key, target) => {
|
||||
const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
|
||||
const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => {
|
||||
const startTime = Date.now();
|
||||
const offset = node[key];
|
||||
const gap = target - offset;
|
||||
|
@ -28,5 +27,5 @@ const scroll = (node, key, target) => {
|
|||
|
||||
const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style;
|
||||
|
||||
export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
|
||||
export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
|
||||
export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
|
||||
export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
|
|
@ -1,15 +1,16 @@
|
|||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import thunk from 'redux-thunk';
|
||||
import appReducer from '../reducers';
|
||||
import loadingBarMiddleware from '../middleware/loading_bar';
|
||||
import errorsMiddleware from '../middleware/errors';
|
||||
import soundsMiddleware from '../middleware/sounds';
|
||||
|
||||
export default function configureStore() {
|
||||
return createStore(appReducer, compose(applyMiddleware(
|
||||
export const store = configureStore({
|
||||
reducer: appReducer,
|
||||
middleware: [
|
||||
thunk,
|
||||
loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
|
||||
errorsMiddleware(),
|
||||
soundsMiddleware(),
|
||||
), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f));
|
||||
}
|
||||
],
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const decode = base64 => {
|
||||
export const decode = (base64: string): Uint8Array => {
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export const toServerSideType = columnType => {
|
||||
export const toServerSideType = (columnType: string) => {
|
||||
switch (columnType) {
|
||||
case 'home':
|
||||
case 'notifications':
|
|
@ -1,23 +1,19 @@
|
|||
// @ts-check
|
||||
import type { ValueOf } from '../../types/util';
|
||||
|
||||
export const DECIMAL_UNITS = Object.freeze({
|
||||
ONE: 1,
|
||||
TEN: 10,
|
||||
HUNDRED: Math.pow(10, 2),
|
||||
THOUSAND: Math.pow(10, 3),
|
||||
MILLION: Math.pow(10, 6),
|
||||
BILLION: Math.pow(10, 9),
|
||||
TRILLION: Math.pow(10, 12),
|
||||
HUNDRED: 100,
|
||||
THOUSAND: 1_000,
|
||||
MILLION: 1_000_000,
|
||||
BILLION: 1_000_000_000,
|
||||
TRILLION: 1_000_000_000_000,
|
||||
});
|
||||
export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>;
|
||||
|
||||
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
|
||||
const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
|
||||
|
||||
/**
|
||||
* @typedef {[number, number, number]} ShortNumber
|
||||
* Array of: shorten number, unit of shorten number and maximum fraction digits
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {number} sourceNumber Number to convert to short number
|
||||
* @returns {ShortNumber} Calculated short number
|
||||
|
@ -25,7 +21,8 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
|
|||
* shortNumber(5936);
|
||||
* // => [5.936, 1000, 1]
|
||||
*/
|
||||
export function toShortNumber(sourceNumber) {
|
||||
export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits
|
||||
export function toShortNumber(sourceNumber: number): ShortNumber {
|
||||
if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
|
||||
return [sourceNumber, DECIMAL_UNITS.ONE, 0];
|
||||
} else if (sourceNumber < DECIMAL_UNITS.MILLION) {
|
||||
|
@ -59,20 +56,16 @@ export function toShortNumber(sourceNumber) {
|
|||
* pluralReady(1793, DECIMAL_UNITS.THOUSAND)
|
||||
* // => 1790
|
||||
*/
|
||||
export function pluralReady(sourceNumber, division) {
|
||||
export function pluralReady(sourceNumber: number, division: DecimalUnits): number {
|
||||
if (division == null || division < DECIMAL_UNITS.HUNDRED) {
|
||||
return sourceNumber;
|
||||
}
|
||||
|
||||
let closestScale = division / DECIMAL_UNITS.TEN;
|
||||
const closestScale = division / DECIMAL_UNITS.TEN;
|
||||
|
||||
return Math.trunc(sourceNumber / closestScale) * closestScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} num
|
||||
* @returns {number}
|
||||
*/
|
||||
export function roundTo10(num) {
|
||||
export function roundTo10(num: number): number {
|
||||
return Math.round(num * 0.1) / 0.1;
|
||||
}
|
|
@ -547,7 +547,7 @@ ul.rules-list {
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 697px) {
|
||||
@media (width <= 697px) {
|
||||
.email-container,
|
||||
.col-1,
|
||||
.col-2,
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
@media screen and (width <= 600px) {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +158,7 @@
|
|||
color: lighten($inverted-text-color, 10%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
@media screen and (width <= 700px) {
|
||||
padding: 30px 20px;
|
||||
|
||||
.page {
|
||||
|
|
|
@ -1362,7 +1362,7 @@ a.sparkline {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 930px) {
|
||||
@media screen and (width <= 930px) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
@ -1648,7 +1648,7 @@ a.sparkline {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
@media screen and (width <= 800px) {
|
||||
border: 0;
|
||||
|
||||
&__item {
|
||||
|
|
|
@ -521,7 +521,7 @@ body > [data-popper-placement] {
|
|||
outline: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
@media screen and (width <= 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
@ -542,7 +542,7 @@ body > [data-popper-placement] {
|
|||
all: unset;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
@media screen and (width <= 600px) {
|
||||
height: 100px !important; // Prevent auto-resize textarea
|
||||
resize: vertical;
|
||||
}
|
||||
|
@ -1838,7 +1838,6 @@ a.account__display-name {
|
|||
.status__avatar {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
box-shadow: 0 0 0 2px $ui-base-color;
|
||||
}
|
||||
|
||||
.muted {
|
||||
|
@ -2468,7 +2467,7 @@ $ui-header-height: 55px;
|
|||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 320px) {
|
||||
@media screen and (width >= 320px) {
|
||||
.logo--wordmark {
|
||||
display: block;
|
||||
}
|
||||
|
@ -2580,7 +2579,7 @@ $ui-header-height: 55px;
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 631px) {
|
||||
@media screen and (width >= 631px) {
|
||||
.columns-area {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -2640,7 +2639,7 @@ $ui-header-height: 55px;
|
|||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
@media screen and (min-width: 631px) {
|
||||
@media screen and (width >= 631px) {
|
||||
background: lighten($ui-base-color, 14%);
|
||||
border-bottom-color: lighten($ui-base-color, 14%);
|
||||
}
|
||||
|
@ -2657,7 +2656,7 @@ $ui-header-height: 55px;
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
@media screen and (width >= 600px) {
|
||||
.tabs-bar__link {
|
||||
span {
|
||||
display: inline;
|
||||
|
@ -2880,7 +2879,7 @@ $ui-header-height: 55px;
|
|||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
@media screen and (width >= 600px) {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
|
@ -2990,7 +2989,7 @@ $ui-header-height: 55px;
|
|||
height: 36px;
|
||||
color: $dark-text-color;
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
@media screen and (width >= 600px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
@ -3042,7 +3041,7 @@ $ui-header-height: 55px;
|
|||
position: sticky;
|
||||
background: $ui-base-color;
|
||||
|
||||
@media screen and (min-width: 600) {
|
||||
@media screen and (width >= 600) {
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
|
@ -3164,6 +3163,10 @@ $ui-header-height: 55px;
|
|||
}
|
||||
|
||||
.compose-form__highlightable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 0 1 auto;
|
||||
border-radius: 4px;
|
||||
transition: box-shadow 300ms linear;
|
||||
|
||||
|
@ -3310,7 +3313,7 @@ $ui-header-height: 55px;
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
@media screen and (min-height: 640px) {
|
||||
@media screen and (height >= 640px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
@ -3659,19 +3662,19 @@ $ui-header-height: 55px;
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 810px) {
|
||||
@media screen and (height <= 810px) {
|
||||
.trends__item:nth-of-type(3) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 720px) {
|
||||
@media screen and (height <= 720px) {
|
||||
.trends__item:nth-of-type(2) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 670px) {
|
||||
@media screen and (height <= 670px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -3755,7 +3758,7 @@ $ui-header-height: 55px;
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
@media screen and (width <= 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
@ -3858,6 +3861,10 @@ a.status-card {
|
|||
}
|
||||
|
||||
.status-card-video {
|
||||
// Firefox has a bug where frameborder=0 iframes add some extra blank space
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=155174
|
||||
overflow: hidden;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -4555,7 +4562,7 @@ a.status-card.compact:hover {
|
|||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
@media screen and (width <= 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
@ -5910,7 +5917,7 @@ a.status-card.compact:hover {
|
|||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
|
||||
@media screen and (max-height: 800px) {
|
||||
@media screen and (height <= 800px) {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
@ -6097,7 +6104,7 @@ a.status-card.compact:hover {
|
|||
display: flex;
|
||||
border-top: 1px solid $ui-secondary-color;
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
@media screen and (width <= 480px) {
|
||||
flex-wrap: wrap;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
@ -6108,7 +6115,7 @@ a.status-card.compact:hover {
|
|||
box-sizing: border-box;
|
||||
width: 50%;
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
@media screen and (width <= 480px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -6130,13 +6137,13 @@ a.status-card.compact:hover {
|
|||
color: $inverted-text-color;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
@media screen and (width <= 480px) {
|
||||
max-height: 10vh;
|
||||
}
|
||||
}
|
||||
|
||||
.focal-point-modal__content {
|
||||
@media screen and (max-width: 480px) {
|
||||
@media screen and (width <= 480px) {
|
||||
max-height: 40vh;
|
||||
}
|
||||
}
|
||||
|
@ -6187,7 +6194,7 @@ a.status-card.compact:hover {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
@media screen and (width <= 480px) {
|
||||
padding: 10px;
|
||||
max-width: 100%;
|
||||
order: 2;
|
||||
|
@ -6405,30 +6412,25 @@ a.status-card.compact:hover {
|
|||
z-index: 9999;
|
||||
}
|
||||
|
||||
.media-gallery__gifv__label {
|
||||
display: block;
|
||||
.media-gallery__item__badges {
|
||||
position: absolute;
|
||||
color: $primary-text-color;
|
||||
background: rgba($base-overlay-background, 0.5);
|
||||
bottom: 6px;
|
||||
inset-inline-start: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.media-gallery__gifv {
|
||||
&:hover {
|
||||
.media-gallery__gifv__label {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.media-gallery__gifv__label {
|
||||
display: block;
|
||||
color: $white;
|
||||
background: rgba($black, 0.65);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
|
@ -6503,17 +6505,28 @@ a.status-card.compact:hover {
|
|||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 64px;
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
grid-template-rows: 50% 50%;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.media-gallery__item {
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
float: left;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
&--tall {
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
&--wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
&.standalone {
|
||||
.media-gallery__item-gifv-thumbnail {
|
||||
transform: none;
|
||||
|
@ -7217,7 +7230,7 @@ noscript {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 630px) and (max-height: 400px) {
|
||||
@media screen and (width <= 630px) and (height <= 400px) {
|
||||
$duration: 400ms;
|
||||
$delay: 100ms;
|
||||
|
||||
|
@ -7347,7 +7360,7 @@ noscript {
|
|||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
@media screen and (width <= 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
@ -7438,7 +7451,7 @@ noscript {
|
|||
width: 380px;
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (max-width: 420px) {
|
||||
@media screen and (width <= 420px) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
|
@ -7493,7 +7506,7 @@ noscript {
|
|||
width: 380px;
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (max-width: 420px) {
|
||||
@media screen and (width <= 420px) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
|
@ -7592,7 +7605,7 @@ noscript {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
@media screen and (width <= 480px) {
|
||||
img,
|
||||
video {
|
||||
max-height: 100%;
|
||||
|
@ -8412,6 +8425,7 @@ noscript {
|
|||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
color: $darker-text-color;
|
||||
aspect-ratio: 16 / 9;
|
||||
|
||||
i {
|
||||
display: block;
|
||||
|
@ -9150,7 +9164,7 @@ noscript {
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
@media screen and (width <= 600px) {
|
||||
display: block;
|
||||
|
||||
h4 {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
width: 700px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media screen and (max-width: 740px) {
|
||||
@media screen and (width <= 740px) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -44,7 +44,7 @@
|
|||
margin-top: 40px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
@media screen and (width <= 400px) {
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
padding: 20px;
|
||||
|
@ -64,7 +64,7 @@
|
|||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid $ui-base-color;
|
||||
|
||||
@media screen and (max-width: 440px) {
|
||||
@media screen and (width <= 440px) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-gap: 10px;
|
||||
|
||||
@media screen and (max-width: 1350px) {
|
||||
@media screen and (width <= 1350px) {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
|
|
|
@ -722,7 +722,7 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 740px) and (min-width: 441px) {
|
||||
@media screen and (440px < width <= 740px) {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
@media screen and (width <= 600px) {
|
||||
.account-header {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 740px) {
|
||||
@media screen and (width <= 740px) {
|
||||
.detailed-status,
|
||||
.status,
|
||||
.load-more {
|
||||
|
|
|
@ -362,7 +362,7 @@ a.table-action-link {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 870px) {
|
||||
@media screen and (width <= 870px) {
|
||||
.accounts-table tbody td.optional {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
interface MastodonMap<T> {
|
||||
get<K extends keyof T>(key: K): T[K];
|
||||
has<K extends keyof T>(key: K): boolean;
|
||||
set<K extends keyof T>(key: K, value: T[K]): this;
|
||||
}
|
||||
import type { Record } from 'immutable';
|
||||
|
||||
type AccountValues = {
|
||||
id: number;
|
||||
|
@ -10,4 +6,5 @@ type AccountValues = {
|
|||
avatar_static: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type Account = MastodonMap<AccountValues>;
|
||||
|
||||
export type Account = Record<AccountValues>;
|
||||
|
|
1
app/javascript/types/util.ts
Normal file
1
app/javascript/types/util.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type ValueOf<T> = T[keyof T];
|
Loading…
Add table
Add a link
Reference in a new issue