Conflicts: - `package.json`: Not really a conflict, just some glitch-soc-specific dependency too close to an upstream-updated one.main
commit
e9ad99bc93
@ -0,0 +1,62 @@
|
||||
// @ts-check
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
/**
|
||||
* Returns custom renderer for one of the common counter types
|
||||
*
|
||||
* @param {"statuses" | "following" | "followers"} counterType
|
||||
* Type of the counter
|
||||
* @param {boolean} isBold Whether display number must be displayed in bold
|
||||
* @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
||||
* Renderer function
|
||||
* @throws If counterType is not covered by this function
|
||||
*/
|
||||
export function counterRenderer(counterType, isBold = true) {
|
||||
/**
|
||||
* @type {(displayNumber: JSX.Element) => JSX.Element}
|
||||
*/
|
||||
const renderCounter = isBold
|
||||
? (displayNumber) => <strong>{displayNumber}</strong>
|
||||
: (displayNumber) => displayNumber;
|
||||
|
||||
switch (counterType) {
|
||||
case 'statuses': {
|
||||
return (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='account.statuses_counter'
|
||||
defaultMessage='{count, plural, one {{counter} Toot} other {{counter} Toots}}'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: renderCounter(displayNumber),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'following': {
|
||||
return (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='account.following_counter'
|
||||
defaultMessage='{count, plural, other {{counter} Following}}'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: renderCounter(displayNumber),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'followers': {
|
||||
return (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='account.followers_counter'
|
||||
defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: renderCounter(displayNumber),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`);
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @callback ShortNumberRenderer
|
||||
* @param {JSX.Element} displayNumber Number to display
|
||||
* @param {number} pluralReady Number used for pluralization
|
||||
* @returns {JSX.Element} Final render of number
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ShortNumberProps
|
||||
* @property {number} value Number to display in short variant
|
||||
* @property {ShortNumberRenderer} [renderer]
|
||||
* Custom renderer for numbers, provided as a prop. If another renderer
|
||||
* passed as a child of this component, this prop won't be used.
|
||||
* @property {ShortNumberRenderer} [children]
|
||||
* Custom renderer for numbers, provided as a child. If another renderer
|
||||
* passed as a prop of this component, this one will be used instead.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Component that renders short big number to a shorter version
|
||||
*
|
||||
* @param {ShortNumberProps} param0 Props for the component
|
||||
* @returns {JSX.Element} Rendered number
|
||||
*/
|
||||
function ShortNumber({ value, renderer, children }) {
|
||||
const shortNumber = toShortNumber(value);
|
||||
const [, division] = shortNumber;
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (children != null && renderer != null) {
|
||||
console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const customRenderer = children != null ? children : renderer;
|
||||
|
||||
const displayNumber = <ShortNumberCounter value={shortNumber} />;
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return customRenderer != null
|
||||
? customRenderer(displayNumber, pluralReady(value, division))
|
||||
: displayNumber;
|
||||
}
|
||||
|
||||
ShortNumber.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
renderer: PropTypes.func,
|
||||
children: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} ShortNumberCounterProps
|
||||
* @property {import('../utils/number').ShortNumber} value Short number
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renders short number into corresponding localizable react fragment
|
||||
*
|
||||
* @param {ShortNumberCounterProps} param0 Props for the component
|
||||
* @returns {JSX.Element} FormattedMessage ready to be embedded in code
|
||||
*/
|
||||
function ShortNumberCounter({ value }) {
|
||||
const [rawNumber, unit, maxFractionDigits = 0] = value;
|
||||
|
||||
const count = (
|
||||
<FormattedNumber
|
||||
value={rawNumber}
|
||||
maximumFractionDigits={maxFractionDigits}
|
||||
/>
|
||||
);
|
||||
|
||||
let values = { count, rawNumber };
|
||||
|
||||
switch (unit) {
|
||||
case DECIMAL_UNITS.THOUSAND: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='units.short.thousand'
|
||||
defaultMessage='{count}K'
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case DECIMAL_UNITS.MILLION: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='units.short.million'
|
||||
defaultMessage='{count}M'
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case DECIMAL_UNITS.BILLION: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='units.short.billion'
|
||||
defaultMessage='{count}B'
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// Not sure if we should go farther - @Sasha-Sorokin
|
||||
default: return count;
|
||||
}
|
||||
}
|
||||
|
||||
ShortNumberCounter.propTypes = {
|
||||
value: PropTypes.arrayOf(PropTypes.number),
|
||||
};
|
||||
|
||||
export default React.memo(ShortNumber);
|
@ -1,34 +1,17 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { changeAccountNoteComment, submitAccountNote, initEditAccountNote, cancelAccountNote } from 'mastodon/actions/account_notes';
|
||||
import { submitAccountNote } from 'mastodon/actions/account_notes';
|
||||
import AccountNote from '../components/account_note';
|
||||
|
||||
const mapStateToProps = (state, { account }) => {
|
||||
const isEditing = state.getIn(['account_notes', 'edit', 'account_id']) === account.get('id');
|
||||
|
||||
return {
|
||||
isSubmitting: state.getIn(['account_notes', 'edit', 'isSubmitting']),
|
||||
accountNote: isEditing ? state.getIn(['account_notes', 'edit', 'comment']) : account.getIn(['relationship', 'note']),
|
||||
isEditing,
|
||||
};
|
||||
};
|
||||
const mapStateToProps = (state, { account }) => ({
|
||||
value: account.getIn(['relationship', 'note']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||
|
||||
onEditAccountNote() {
|
||||
dispatch(initEditAccountNote(account));
|
||||
},
|
||||
|
||||
onSaveAccountNote() {
|
||||
dispatch(submitAccountNote());
|
||||
onSave (value) {
|
||||
dispatch(submitAccountNote(account.get('id'), value));
|
||||
},
|
||||
|
||||
onCancelAccountNote() {
|
||||
dispatch(cancelAccountNote());
|
||||
},
|
||||
|
||||
onChangeAccountNote(comment) {
|
||||
dispatch(changeAccountNoteComment(comment));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);
|
||||
|
@ -1,44 +0,0 @@
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import {
|
||||
ACCOUNT_NOTE_INIT_EDIT,
|
||||
ACCOUNT_NOTE_CANCEL,
|
||||
ACCOUNT_NOTE_CHANGE_COMMENT,
|
||||
ACCOUNT_NOTE_SUBMIT_REQUEST,
|
||||
ACCOUNT_NOTE_SUBMIT_FAIL,
|
||||
ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
||||
} from '../actions/account_notes';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
edit: ImmutableMap({
|
||||
isSubmitting: false,
|
||||
account_id: null,
|
||||
comment: null,
|
||||
}),
|
||||
});
|
||||
|
||||
export default function account_notes(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case ACCOUNT_NOTE_INIT_EDIT:
|
||||
return state.withMutations((state) => {
|
||||
state.setIn(['edit', 'isSubmitting'], false);
|
||||
state.setIn(['edit', 'account_id'], action.account.get('id'));
|
||||
state.setIn(['edit', 'comment'], action.comment);
|
||||
});
|
||||
case ACCOUNT_NOTE_CHANGE_COMMENT:
|
||||
return state.setIn(['edit', 'comment'], action.comment);
|
||||
case ACCOUNT_NOTE_SUBMIT_REQUEST:
|
||||
return state.setIn(['edit', 'isSubmitting'], true);
|
||||
case ACCOUNT_NOTE_SUBMIT_FAIL:
|
||||
return state.setIn(['edit', 'isSubmitting'], false);
|
||||
case ACCOUNT_NOTE_SUBMIT_SUCCESS:
|
||||
case ACCOUNT_NOTE_CANCEL:
|
||||
return state.withMutations((state) => {
|
||||
state.setIn(['edit', 'isSubmitting'], false);
|
||||
state.setIn(['edit', 'account_id'], null);
|
||||
state.setIn(['edit', 'comment'], null);
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -1,16 +1,71 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { FormattedNumber } from 'react-intl';
|
||||
|
||||
export const shortNumberFormat = number => {
|
||||
if (number < 1000) {
|
||||
return <FormattedNumber value={number} />;
|
||||
} else if (number < 10000) {
|
||||
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>;
|
||||
} else if (number < 1000000) {
|
||||
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={0} />K</Fragment>;
|
||||
} else if (number < 10000000) {
|
||||
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>;
|
||||
} else {
|
||||
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={0} />M</Fragment>;
|
||||
// @ts-check
|
||||
|
||||
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),
|
||||
});
|
||||
|
||||
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
|
||||
* @example
|
||||
* shortNumber(5936);
|
||||
* // => [5.936, 1000, 1]
|
||||
*/
|
||||
export function toShortNumber(sourceNumber) {
|
||||
if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
|
||||
return [sourceNumber, DECIMAL_UNITS.ONE, 0];
|
||||
} else if (sourceNumber < DECIMAL_UNITS.MILLION) {
|
||||
return [
|
||||
sourceNumber / DECIMAL_UNITS.THOUSAND,
|
||||
DECIMAL_UNITS.THOUSAND,
|
||||
sourceNumber < TEN_THOUSAND ? 1 : 0,
|
||||
];
|
||||
} else if (sourceNumber < DECIMAL_UNITS.BILLION) {
|
||||
return [
|
||||
sourceNumber / DECIMAL_UNITS.MILLION,
|
||||
DECIMAL_UNITS.MILLION,
|
||||
sourceNumber < TEN_MILLIONS ? 1 : 0,
|
||||
];
|
||||
} else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
|
||||
return [
|
||||
sourceNumber / DECIMAL_UNITS.BILLION,
|
||||
DECIMAL_UNITS.BILLION,
|
||||
0,
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
return [sourceNumber, DECIMAL_UNITS.ONE, 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} sourceNumber Original number that is shortened
|
||||
* @param {number} division The scale in which short number is displayed
|
||||
* @returns {number} Number that can be used for plurals when short form used
|
||||
* @example
|
||||
* pluralReady(1793, DECIMAL_UNITS.THOUSAND)
|
||||
* // => 1790
|
||||
*/
|
||||
export function pluralReady(sourceNumber, division) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (division == null || division < DECIMAL_UNITS.HUNDRED) {
|
||||
return sourceNumber;
|
||||
}
|
||||
|
||||
let closestScale = division / DECIMAL_UNITS.TEN;
|
||||
|
||||
return Math.trunc(sourceNumber / closestScale) * closestScale;
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
class MediaAttachmentIdsToTimestampIds < ActiveRecord::Migration[5.1]
|
||||
def up
|
||||
# Set up the media_attachments.id column to use our timestamp-based IDs.
|
||||
safety_assured do
|
||||
execute("ALTER TABLE media_attachments ALTER COLUMN id SET DEFAULT timestamp_id('media_attachments')")
|
||||
end
|
||||
|
||||
# Make sure we have a sequence to use.
|
||||
Mastodon::Snowflake.ensure_id_sequences_exist
|
||||
end
|
||||
|
||||
def down
|
||||
execute("LOCK media_attachments")
|
||||
execute("SELECT setval('media_attachments_id_seq', (SELECT MAX(id) FROM media_attachments))")
|
||||
execute("ALTER TABLE media_attachments ALTER COLUMN id SET DEFAULT nextval('media_attachments_id_seq')")
|
||||
end
|
||||
end
|
@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe MediaProxyController do
|
||||
render_views
|
||||
|
||||
before do
|
||||
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
|
||||
end
|
||||
|
||||
describe '#show' do
|
||||
it 'redirects when attached to a status' do
|
||||
status = Fabricate(:status)
|
||||
media_attachment = Fabricate(:media_attachment, status: status, remote_url: 'http://example.com/attachment.png')
|
||||
get :show, params: { id: media_attachment.id }
|
||||
|
||||
expect(response).to have_http_status(302)
|
||||
end
|
||||
|
||||
it 'responds with missing when there is not an attached status' do
|
||||
media_attachment = Fabricate(:media_attachment, status: nil, remote_url: 'http://example.com/attachment.png')
|
||||
get :show, params: { id: media_attachment.id }
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
||||
it 'raises when id cant be found' do
|
||||
get :show, params: { id: 'missing' }
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
||||
it 'raises when not permitted to view' do
|
||||
status = Fabricate(:status, visibility: :direct)
|
||||
media_attachment = Fabricate(:media_attachment, status: status, remote_url: 'http://example.com/attachment.png')
|
||||
get :show, params: { id: media_attachment.id }
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in new issue