Merge commit '322ce1fc27
' into kb_patch
This commit is contained in:
commit
1ac499aefe
102 changed files with 1827 additions and 495 deletions
app/javascript
mastodon
actions
api_types
components
containers
features
about
circle_statuses
circles
compose
components
containers
mentioned_users
report/components
status/components
ui
locales
reducers
selectors
utils
styles/mastodon
|
@ -1,7 +1,7 @@
|
|||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { showAlertForError } from './alerts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
|
||||
export const CIRCLE_FETCH_REQUEST = 'CIRCLE_FETCH_REQUEST';
|
||||
export const CIRCLE_FETCH_SUCCESS = 'CIRCLE_FETCH_SUCCESS';
|
||||
|
@ -50,6 +50,14 @@ export const CIRCLE_ADDER_CIRCLES_FETCH_REQUEST = 'CIRCLE_ADDER_CIRCLES_FETCH_RE
|
|||
export const CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS = 'CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS';
|
||||
export const CIRCLE_ADDER_CIRCLES_FETCH_FAIL = 'CIRCLE_ADDER_CIRCLES_FETCH_FAIL';
|
||||
|
||||
export const CIRCLE_STATUSES_FETCH_REQUEST = 'CIRCLE_STATUSES_FETCH_REQUEST';
|
||||
export const CIRCLE_STATUSES_FETCH_SUCCESS = 'CIRCLE_STATUSES_FETCH_SUCCESS';
|
||||
export const CIRCLE_STATUSES_FETCH_FAIL = 'CIRCLE_STATUSES_FETCH_FAIL';
|
||||
|
||||
export const CIRCLE_STATUSES_EXPAND_REQUEST = 'CIRCLE_STATUSES_EXPAND_REQUEST';
|
||||
export const CIRCLE_STATUSES_EXPAND_SUCCESS = 'CIRCLE_STATUSES_EXPAND_SUCCESS';
|
||||
export const CIRCLE_STATUSES_EXPAND_FAIL = 'CIRCLE_STATUSES_EXPAND_FAIL';
|
||||
|
||||
export const fetchCircle = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['circles', id])) {
|
||||
return;
|
||||
|
@ -370,3 +378,89 @@ export const removeFromCircleAdder = circleId => (dispatch, getState) => {
|
|||
dispatch(removeFromCircle(circleId, getState().getIn(['circleAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export function fetchCircleStatuses(circleId) {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().getIn(['circles', circleId, 'statuses', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchCircleStatusesRequest(circleId));
|
||||
|
||||
api(getState).get(`/api/v1/circles/${circleId}/statuses`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchCircleStatusesSuccess(circleId, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchCircleStatusesFail(circleId, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCircleStatusesRequest(id) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_FETCH_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCircleStatusesSuccess(id, statuses, next) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_FETCH_SUCCESS,
|
||||
id,
|
||||
statuses,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCircleStatusesFail(id, error) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandCircleStatuses(circleId) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['circles', circleId, 'statuses', 'next'], null);
|
||||
|
||||
if (url === null || getState().getIn(['circles', circleId, 'statuses', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandCircleStatusesRequest(circleId));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandCircleStatusesSuccess(circleId, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandCircleStatusesFail(circleId, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function expandCircleStatusesRequest(id) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandCircleStatusesSuccess(id, statuses, next) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_EXPAND_SUCCESS,
|
||||
id,
|
||||
statuses,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandCircleStatusesFail(id, error) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,8 @@ export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
|||
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
||||
export const COMPOSE_RESET = 'COMPOSE_RESET';
|
||||
|
||||
export const COMPOSE_WITH_CIRCLE_SUCCESS = 'COMPOSE_WITH_CIRCLE_SUCCESS';
|
||||
|
||||
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
|
||||
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
||||
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
||||
|
@ -174,6 +176,7 @@ export function submitCompose(routerHistory) {
|
|||
const status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const statusId = getState().getIn(['compose', 'id'], null);
|
||||
const circleId = getState().getIn(['compose', 'circle_id'], null);
|
||||
|
||||
if ((!status || !status.length) && media.size === 0) {
|
||||
return;
|
||||
|
@ -253,6 +256,10 @@ export function submitCompose(routerHistory) {
|
|||
insertIfOnline(`account:${response.data.account.id}`);
|
||||
}
|
||||
|
||||
if (statusId === null && circleId !== null && circleId !== 0) {
|
||||
dispatch(submitComposeWithCircleSuccess({ ...response.data }, circleId));
|
||||
}
|
||||
|
||||
dispatch(showAlert({
|
||||
message: statusId === null ? messages.published : messages.saved,
|
||||
action: messages.open,
|
||||
|
@ -278,6 +285,14 @@ export function submitComposeSuccess(status) {
|
|||
};
|
||||
}
|
||||
|
||||
export function submitComposeWithCircleSuccess(status, circleId) {
|
||||
return {
|
||||
type: COMPOSE_WITH_CIRCLE_SUCCESS,
|
||||
status,
|
||||
circleId,
|
||||
}
|
||||
}
|
||||
|
||||
export function submitComposeFail(error) {
|
||||
return {
|
||||
type: COMPOSE_SUBMIT_FAIL,
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
export function openDropdownMenu(id, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
|
||||
}
|
||||
|
||||
export function closeDropdownMenu(id) {
|
||||
return { type: DROPDOWN_MENU_CLOSE, id };
|
||||
}
|
11
app/javascript/mastodon/actions/dropdown_menu.ts
Normal file
11
app/javascript/mastodon/actions/dropdown_menu.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const openDropdownMenu = createAction<{
|
||||
id: string;
|
||||
keyboard: boolean;
|
||||
scrollKey: string;
|
||||
}>('dropdownMenu/open');
|
||||
|
||||
export const closeDropdownMenu = createAction<{ id: string }>(
|
||||
'dropdownMenu/close',
|
||||
);
|
|
@ -71,6 +71,14 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
|||
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||
|
||||
export const MENTIONED_USERS_FETCH_REQUEST = 'MENTIONED_USERS_FETCH_REQUEST';
|
||||
export const MENTIONED_USERS_FETCH_SUCCESS = 'MENTIONED_USERS_FETCH_SUCCESS';
|
||||
export const MENTIONED_USERS_FETCH_FAIL = 'MENTIONED_USERS_FETCH_FAIL';
|
||||
|
||||
export const MENTIONED_USERS_EXPAND_REQUEST = 'MENTIONED_USERS_EXPAND_REQUEST';
|
||||
export const MENTIONED_USERS_EXPAND_SUCCESS = 'MENTIONED_USERS_EXPAND_SUCCESS';
|
||||
export const MENTIONED_USERS_EXPAND_FAIL = 'MENTIONED_USERS_EXPAND_FAIL';
|
||||
|
||||
export function reblog(status, visibility) {
|
||||
return function (dispatch, getState) {
|
||||
dispatch(reblogRequest(status));
|
||||
|
@ -735,3 +743,85 @@ export function unpinFail(status, error) {
|
|||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMentionedUsers(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchMentionedUsersRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/mentioned_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchMentionedUsersSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchMentionedUsersFail(id, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMentionedUsersRequest(id) {
|
||||
return {
|
||||
type: MENTIONED_USERS_FETCH_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMentionedUsersSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: MENTIONED_USERS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMentionedUsersFail(id, error) {
|
||||
return {
|
||||
type: MENTIONED_USERS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandMentionedUsers(id) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'mentioned_users', id, 'next']);
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandMentionedUsersRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandMentionedUsersSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(expandMentionedUsersFail(id, error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function expandMentionedUsersRequest(id) {
|
||||
return {
|
||||
type: MENTIONED_USERS_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandMentionedUsersSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: MENTIONED_USERS_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandMentionedUsersFail(id, error) {
|
||||
return {
|
||||
type: MENTIONED_USERS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { ModalProps } from 'mastodon/reducers/modal';
|
||||
|
||||
import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
|
||||
|
||||
export type ModalType = keyof typeof MODAL_COMPONENTS;
|
||||
|
||||
interface OpenModalPayload {
|
||||
modalType: ModalType;
|
||||
modalProps: unknown;
|
||||
modalProps: ModalProps;
|
||||
}
|
||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
||||
|
||||
|
|
65
app/javascript/mastodon/api_types/accounts.ts
Normal file
65
app/javascript/mastodon/api_types/accounts.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||
|
||||
export interface ApiAccountFieldJSON {
|
||||
name: string;
|
||||
value: string;
|
||||
verified_at: string | null;
|
||||
}
|
||||
|
||||
export interface ApiAccountRoleJSON {
|
||||
color: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ApiAccountOtherSettingsJSON {
|
||||
noindex: boolean;
|
||||
noai: boolean;
|
||||
hide_network: boolean;
|
||||
hide_statuses_count: boolean;
|
||||
hide_following_count: boolean;
|
||||
hide_followers_count: boolean;
|
||||
translatable_private: boolean;
|
||||
link_preview: boolean;
|
||||
emoji_reaction_policy?:
|
||||
| 'allow'
|
||||
| 'outside_only'
|
||||
| 'following_only'
|
||||
| 'followers_only'
|
||||
| 'mutuals_only'
|
||||
| 'block';
|
||||
}
|
||||
|
||||
// See app/serializers/rest/account_serializer.rb
|
||||
export interface ApiAccountJSON {
|
||||
acct: string;
|
||||
avatar: string;
|
||||
avatar_static: string;
|
||||
bot: boolean;
|
||||
created_at: string;
|
||||
discoverable: boolean;
|
||||
display_name: string;
|
||||
emojis: ApiCustomEmojiJSON[];
|
||||
fields: ApiAccountFieldJSON[];
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
group: boolean;
|
||||
header: string;
|
||||
header_static: string;
|
||||
id: string;
|
||||
last_status_at: string;
|
||||
locked: boolean;
|
||||
noindex: boolean;
|
||||
note: string;
|
||||
other_settings: ApiAccountOtherSettingsJSON;
|
||||
roles: ApiAccountJSON[];
|
||||
subscribable: boolean;
|
||||
statuses_count: number;
|
||||
uri: string;
|
||||
url: string;
|
||||
username: string;
|
||||
moved?: ApiAccountJSON;
|
||||
suspended?: boolean;
|
||||
limited?: boolean;
|
||||
memorial?: boolean;
|
||||
}
|
12
app/javascript/mastodon/api_types/custom_emoji.ts
Normal file
12
app/javascript/mastodon/api_types/custom_emoji.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
// See app/serializers/rest/account_serializer.rb
|
||||
export interface ApiCustomEmojiJSON {
|
||||
shortcode: string;
|
||||
static_url: string;
|
||||
url: string;
|
||||
category?: string;
|
||||
visible_in_picker: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
sensitive?: boolean;
|
||||
aliases?: string[];
|
||||
}
|
18
app/javascript/mastodon/api_types/relationships.ts
Normal file
18
app/javascript/mastodon/api_types/relationships.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
// See app/serializers/rest/relationship_serializer.rb
|
||||
export interface ApiRelationshipJSON {
|
||||
blocked_by: boolean;
|
||||
blocking: boolean;
|
||||
domain_blocking: boolean;
|
||||
endorsed: boolean;
|
||||
followed_by: boolean;
|
||||
following: boolean;
|
||||
id: string;
|
||||
languages: string[] | null;
|
||||
muting_notifications: boolean;
|
||||
muting: boolean;
|
||||
note: string;
|
||||
notifying: boolean;
|
||||
requested_by: boolean;
|
||||
requested: boolean;
|
||||
showing_reblogs: boolean;
|
||||
}
|
|
@ -4,9 +4,14 @@ import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_m
|
|||
import { fetchHistory } from 'mastodon/actions/history';
|
||||
import DropdownMenu from 'mastodon/components/dropdown_menu';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('mastodon/store').RootState} state
|
||||
* @param {*} props
|
||||
*/
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
openDropdownId: state.dropdownMenu.openId,
|
||||
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||
items: state.getIn(['history', statusId, 'items']),
|
||||
loading: state.getIn(['history', statusId, 'loading']),
|
||||
});
|
||||
|
@ -15,11 +20,11 @@ const mapDispatchToProps = (dispatch, { statusId }) => ({
|
|||
|
||||
onOpen (id, onItemClick, keyboard) {
|
||||
dispatch(fetchHistory(statusId));
|
||||
dispatch(openDropdownMenu(id, keyboard));
|
||||
dispatch(openDropdownMenu({ id, keyboard }));
|
||||
},
|
||||
|
||||
onClose (id) {
|
||||
dispatch(closeDropdownMenu(id));
|
||||
dispatch(closeDropdownMenu({ id }));
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -23,9 +23,14 @@ const MOUSE_IDLE_DELAY = 300;
|
|||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('mastodon/store').RootState} state
|
||||
* @param {*} props
|
||||
*/
|
||||
const mapStateToProps = (state, { scrollKey }) => {
|
||||
return {
|
||||
preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
|
||||
preventScroll: scrollKey === state.dropdownMenu.scrollKey,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@ const messages = defineMessages({
|
|||
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
|
||||
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
|
||||
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
|
||||
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||
});
|
||||
|
@ -405,6 +406,7 @@ class Status extends ImmutablePureComponent {
|
|||
'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) },
|
||||
'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) },
|
||||
'circle': { icon: 'user-circle', text: intl.formatMessage(messages.circle_short) },
|
||||
'personal': { icon: 'sticky-note-o', text: intl.formatMessage(messages.personal_short) },
|
||||
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ const messages = defineMessages({
|
|||
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
mentions: { id: 'status.mentions', defaultMessage: 'Mentioned users' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
|
@ -249,6 +250,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
|
||||
};
|
||||
|
||||
handleOpenMentions = () => {
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}/mentioned_users`);
|
||||
};
|
||||
|
||||
handleEmbed = () => {
|
||||
this.props.onEmbed(this.props.status);
|
||||
};
|
||||
|
@ -315,7 +320,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (signedIn) {
|
||||
if (!simpleTimelineMenu) {
|
||||
if (writtenByMe) {
|
||||
menu.push({ text: intl.formatMessage(messages.mentions), action: this.handleOpenMentions });
|
||||
}
|
||||
|
||||
if (!simpleTimelineMenu || writtenByMe) {
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,12 @@ import { openModal, closeModal } from '../actions/modal';
|
|||
import DropdownMenu from '../components/dropdown_menu';
|
||||
import { isUserTouching } from '../is_mobile';
|
||||
|
||||
/**
|
||||
* @param {import('mastodon/store').RootState} state
|
||||
*/
|
||||
const mapStateToProps = state => ({
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
openDropdownId: state.dropdownMenu.openId,
|
||||
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
|
@ -25,7 +28,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
|||
actions: items,
|
||||
onClick: onItemClick,
|
||||
},
|
||||
}) : openDropdownMenu(id, keyboard, scrollKey));
|
||||
}) : openDropdownMenu({ id, keyboard, scrollKey }));
|
||||
},
|
||||
|
||||
onClose(id) {
|
||||
|
@ -33,7 +36,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
|||
modalType: 'ACTIONS',
|
||||
ignoreFocus: false,
|
||||
}));
|
||||
dispatch(closeDropdownMenu(id));
|
||||
dispatch(closeDropdownMenu({ id }));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -28,6 +28,11 @@ const messages = defineMessages({
|
|||
silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' },
|
||||
suspended: { id: 'about.domain_blocks.suspended.title', defaultMessage: 'Suspended' },
|
||||
suspendedExplanation: { id: 'about.domain_blocks.suspended.explanation', defaultMessage: 'No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.' },
|
||||
publicUnlistedVisibility: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
|
||||
emojiReaction: { id: 'status.emoji_reaction', defaultMessage: 'Stamp' },
|
||||
enabled: { id: 'about.enabled', defaultMessage: 'Enabled' },
|
||||
disabled: { id: 'about.disabled', defaultMessage: 'Disabled' },
|
||||
capabilities: { id: 'about.kmyblue_capabilities', defaultMessage: 'kmyblue capabilities' },
|
||||
});
|
||||
|
||||
const severityMessages = {
|
||||
|
@ -122,6 +127,10 @@ class About extends PureComponent {
|
|||
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
|
||||
const isLoading = server.get('isLoading');
|
||||
|
||||
const fedibirdCapabilities = server.get('fedibird_capabilities');
|
||||
const isPublicUnlistedVisibility = fedibirdCapabilities.includes('kmyblue_visibility_public_unlisted');
|
||||
const isEmojiReaction = fedibirdCapabilities.includes('emoji_reaction');
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
||||
<div className='scrollable about'>
|
||||
|
@ -182,6 +191,20 @@ class About extends PureComponent {
|
|||
))}
|
||||
</Section>
|
||||
|
||||
<Section title={intl.formatMessage(messages.capabilities)}>
|
||||
<p><FormattedMessage id='about.kmyblue_capability' defaultMessage='This server is using kmyblue, a fork of Mastodon. On this server, kmyblues unique features are configured as follows.' /></p>
|
||||
{!isLoading && (
|
||||
<ol className='rules-list'>
|
||||
<li>
|
||||
<span className='rules-list__text'>{intl.formatMessage(messages.emojiReaction)}: {intl.formatMessage(isEmojiReaction ? messages.enabled : messages.disabled)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='rules-list__text'>{intl.formatMessage(messages.publicUnlistedVisibility)}: {intl.formatMessage(isPublicUnlistedVisibility ? messages.enabled : messages.disabled)}</span>
|
||||
</li>
|
||||
</ol>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
|
||||
{domainBlocks.get('isLoading') ? (
|
||||
<>
|
||||
|
|
182
app/javascript/mastodon/features/circle_statuses/index.jsx
Normal file
182
app/javascript/mastodon/features/circle_statuses/index.jsx
Normal file
|
@ -0,0 +1,182 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { deleteCircle, expandCircleStatuses, fetchCircle, fetchCircleStatuses } from 'mastodon/actions/circles';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import StatusList from 'mastodon/components/status_list';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import { getCircleStatusList } from 'mastodon/selectors';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete_circle.message', defaultMessage: 'Are you sure you want to permanently delete this circle?' },
|
||||
deleteConfirm: { id: 'confirmations.delete_circle.confirm', defaultMessage: 'Delete' },
|
||||
heading: { id: 'column.circles', defaultMessage: 'Circles' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { params }) => ({
|
||||
circle: state.getIn(['circles', params.id]),
|
||||
statusIds: getCircleStatusList(state, params.id),
|
||||
isLoading: state.getIn(['circles', params.id, 'isLoading'], true),
|
||||
isEditing: state.getIn(['circleEditor', 'circleId']) === params.id,
|
||||
hasMore: !!state.getIn(['circles', params.id, 'next']),
|
||||
});
|
||||
|
||||
class CircleStatuses extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
circle: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
intl: PropTypes.object.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
this.props.dispatch(fetchCircle(this.props.params.id));
|
||||
this.props.dispatch(fetchCircleStatuses(this.props.params.id));
|
||||
}
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('CIRCLE_STATUSES', { id: this.props.params.id }));
|
||||
this.context.router.history.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
handleMove = (dir) => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
};
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'CIRCLE_EDITOR',
|
||||
modalProps: { circleId: this.props.params.id },
|
||||
}));
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, columnId, intl } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteCircle(id));
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
this.context.router.history.push('/circles');
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandCircleStatuses());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, circle, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
if (typeof circle === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<div className='scrollable'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
} else if (circle === false) {
|
||||
return (
|
||||
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.circle_statuses' defaultMessage="You don't have any circle posts yet. When you post one as circle, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
|
||||
<ColumnHeader
|
||||
icon='user-circle'
|
||||
title={circle.get('title')}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<div className='column-settings__row column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<Icon id='pencil' /> <FormattedMessage id='circles.edit' defaultMessage='Edit circle' />
|
||||
</button>
|
||||
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' /> <FormattedMessage id='circles.delete' defaultMessage='Delete circle' />
|
||||
</button>
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusList
|
||||
trackScroll={!pinned}
|
||||
statusIds={statusIds}
|
||||
scrollKey={`circle_statuses-${columnId}`}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(CircleStatuses));
|
|
@ -13,7 +13,6 @@ import { fetchCircles, deleteCircle } from 'mastodon/actions/circles';
|
|||
import { openModal } from 'mastodon/actions/modal';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import ColumnLink from 'mastodon/features/ui/components/column_link';
|
||||
|
@ -106,10 +105,7 @@ class Circles extends ImmutablePureComponent {
|
|||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{circles.map(circle =>
|
||||
(<div key={circle.get('id')} className='circle-item'>
|
||||
<ColumnLink to={`#`} data-id={circle.get('id')} onClick={this.handleEditClick} icon='user-circle' text={circle.get('title')} />
|
||||
<IconButton icon='trash' data_id={circle.get('id')} onClick={this.handleRemoveClick} />
|
||||
</div>)
|
||||
<ColumnLink key={circle.get('id')} to={`/circles/${circle.get('id')}`} icon='user-circle' text={circle.get('title')} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
|
|
|
@ -103,11 +103,11 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
canSubmit = () => {
|
||||
const { isSubmitting, isChangingUpload, isUploading, anyMedia, privacy, circleId } = this.props;
|
||||
const { isSubmitting, isChangingUpload, isUploading, anyMedia, privacy, circleId, isEditing } = this.props;
|
||||
const fulltext = this.getFulltextForCharacterCounting();
|
||||
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
|
||||
|
||||
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia) || (privacy === 'circle' && !circleId));
|
||||
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia) || (privacy === 'circle' && !isEditing && !circleId));
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { supportsPassiveEvents } from 'detect-passive-events';
|
|||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { enableLoginPrivacy } from 'mastodon/initial_state';
|
||||
import { enableLoginPrivacy, enableLocalPrivacy } from 'mastodon/initial_state';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
|
@ -246,6 +246,10 @@ class PrivacyDropdown extends PureComponent {
|
|||
this.selectableOptions = this.selectableOptions.filter((opt) => opt.value !== 'login');
|
||||
}
|
||||
|
||||
if (!enableLocalPrivacy) {
|
||||
this.selectableOptions = this.selectableOptions.filter((opt) => opt.value !== 'public_unlisted');
|
||||
}
|
||||
|
||||
if (this.props.noDirect) {
|
||||
this.selectableOptions = this.selectableOptions.filter((opt) => opt.value !== 'direct');
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { PureComponent } from 'react';
|
|||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
width: `${18 * 1.28571429}px`,
|
||||
minWidth: `${18 * 1.28571429}px`,
|
||||
};
|
||||
|
||||
export default class TextIconButton extends PureComponent {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { changeCircle } from '../../../actions/compose';
|
|||
import CircleSelect from '../components/circle_select';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
unavailable: state.getIn(['compose', 'privacy']) !== 'circle',
|
||||
unavailable: state.getIn(['compose', 'privacy']) !== 'circle' || !!state.getIn(['compose', 'id']),
|
||||
circles: state.get('circles'),
|
||||
circleId: state.getIn(['compose', 'circle_id']),
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { HASHTAG_PATTERN_REGEX } from 'mastodon/utils/hashtags';
|
||||
import { MENTION_PATTERN_REGEX } from 'mastodon/utils/mentions';
|
||||
|
||||
import Warning from '../components/warning';
|
||||
|
||||
|
@ -14,10 +15,11 @@ const mapStateToProps = state => ({
|
|||
hashtagWarning: !['public', 'public_unlisted', 'login'].includes(state.getIn(['compose', 'privacy'])) && state.getIn(['compose', 'searchability']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
|
||||
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
|
||||
searchabilityWarning: state.getIn(['compose', 'searchability']) === 'limited',
|
||||
limitedPostWarning: ['mutual', 'circle'].includes(state.getIn(['compose', 'privacy'])),
|
||||
mentionWarning: ['mutual', 'circle', 'limited'].includes(state.getIn(['compose', 'privacy'])) && MENTION_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
|
||||
limitedPostWarning: ['mutual', 'circle'].includes(state.getIn(['compose', 'privacy'])) && !state.getIn(['compose', 'limited_scope']),
|
||||
});
|
||||
|
||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning, searchabilityWarning, limitedPostWarning }) => {
|
||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning, searchabilityWarning, mentionWarning, limitedPostWarning }) => {
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||
}
|
||||
|
@ -40,6 +42,10 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
|
|||
return <Warning message={<FormattedMessage id='compose_form.searchability_warning' defaultMessage='Self only searchability is not available other mastodon servers. Others can search your post.' />} />;
|
||||
}
|
||||
|
||||
if (mentionWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.mention_warning' defaultMessage='When you add a mention to a limited post, the person you are mentioning can also see this post.' />} />;
|
||||
}
|
||||
|
||||
if (limitedPostWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.limited_post_warning' defaultMessage='Limited posts are NOT reached Misskey, normal Mastodon or so on.' />} />;
|
||||
}
|
||||
|
@ -52,6 +58,7 @@ WarningWrapper.propTypes = {
|
|||
hashtagWarning: PropTypes.bool,
|
||||
directMessageWarning: PropTypes.bool,
|
||||
searchabilityWarning: PropTypes.bool,
|
||||
mentionWarning: PropTypes.bool,
|
||||
limitedPostWarning: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
|
90
app/javascript/mastodon/features/mentioned_users/index.jsx
Normal file
90
app/javascript/mastodon/features/mentioned_users/index.jsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { fetchMentionedUsers, expandMentionedUsers } from 'mastodon/actions/interactions';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import AccountContainer from 'mastodon/containers/account_container';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
accountIds: state.getIn(['user_lists', 'mentioned_users', props.params.statusId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'mentioned_users', props.params.statusId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'mentioned_users', props.params.statusId, 'isLoading'], true),
|
||||
});
|
||||
|
||||
class MentionedUsers extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchMentionedUsers(this.props.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandMentionedUsers(this.props.params.statusId));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, isLoading, multiColumn } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.mentioned_users' defaultMessage='No one has been mentioned by this post.' />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn}>
|
||||
<ColumnHeader
|
||||
showBackButton
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='mentioned_users'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(MentionedUsers));
|
|
@ -23,6 +23,7 @@ const messages = defineMessages({
|
|||
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
|
||||
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
|
||||
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
|
||||
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||
});
|
||||
|
||||
|
@ -57,6 +58,7 @@ class StatusCheckBox extends PureComponent {
|
|||
'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) },
|
||||
'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) },
|
||||
'circle': { icon: 'user-circle', text: intl.formatMessage(messages.circle_short) },
|
||||
'personal': { icon: 'sticky-note-o', text: intl.formatMessage(messages.personal_short) },
|
||||
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ const messages = defineMessages({
|
|||
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
mentions: { id: 'status.mentions', defaultMessage: 'Mentioned users' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||
|
@ -95,6 +96,10 @@ class ActionBar extends PureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleOpenMentions = () => {
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}/mentioned_users`);
|
||||
};
|
||||
|
||||
handleReplyClick = () => {
|
||||
this.props.onReply(this.props.status);
|
||||
};
|
||||
|
@ -264,6 +269,7 @@ class ActionBar extends PureComponent {
|
|||
menu.push(null);
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.mentions), action: this.handleOpenMentions });
|
||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||
|
|
|
@ -35,6 +35,7 @@ const messages = defineMessages({
|
|||
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
|
||||
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
|
||||
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
|
||||
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||
searchability_public_short: { id: 'searchability.public.short', defaultMessage: 'Public' },
|
||||
searchability_private_short: { id: 'searchability.unlisted.short', defaultMessage: 'Followers' },
|
||||
|
@ -260,6 +261,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) },
|
||||
'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) },
|
||||
'circle': { icon: 'user-circle', text: intl.formatMessage(messages.circle_short) },
|
||||
'personal': { icon: 'sticky-note-o', text: intl.formatMessage(messages.personal_short) },
|
||||
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
|
||||
};
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ const messages = defineMessages({
|
|||
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
|
||||
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
|
||||
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
|
||||
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||
});
|
||||
|
||||
|
@ -100,6 +101,7 @@ class BoostModal extends ImmutablePureComponent {
|
|||
'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) },
|
||||
'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) },
|
||||
'circle': { icon: 'user-circle', text: intl.formatMessage(messages.circle_short) },
|
||||
'personal': { icon: 'sticky-note-o', text: intl.formatMessage(messages.personal_short) },
|
||||
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
BookmarkCategoryStatuses,
|
||||
AntennaSetting,
|
||||
AntennaTimeline,
|
||||
CircleStatuses,
|
||||
} from '../util/async-components';
|
||||
|
||||
import BundleColumnError from './bundle_column_error';
|
||||
|
@ -45,6 +46,7 @@ const componentMap = {
|
|||
'EMOJI_REACTIONS': EmojiReactedStatuses,
|
||||
'BOOKMARKS': BookmarkedStatuses,
|
||||
'BOOKMARKS_EX': BookmarkCategoryStatuses,
|
||||
'CIRCLE_STATUSES': CircleStatuses,
|
||||
'ANTENNA': AntennaSetting,
|
||||
'ANTENNA_TIMELINE': AntennaTimeline,
|
||||
'LIST': ListTimeline,
|
||||
|
|
|
@ -46,6 +46,7 @@ import {
|
|||
Favourites,
|
||||
EmojiReactions,
|
||||
StatusReferences,
|
||||
MentionedUsers,
|
||||
DirectTimeline,
|
||||
HashtagTimeline,
|
||||
AntennaTimeline,
|
||||
|
@ -65,6 +66,7 @@ import {
|
|||
Lists,
|
||||
Antennas,
|
||||
Circles,
|
||||
CircleStatuses,
|
||||
AntennaSetting,
|
||||
Directory,
|
||||
Explore,
|
||||
|
@ -90,7 +92,7 @@ const mapStateToProps = state => ({
|
|||
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||
dropdownMenuIsOpen: state.dropdownMenu.openId !== null,
|
||||
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
||||
username: state.getIn(['accounts', me, 'username']),
|
||||
});
|
||||
|
@ -242,6 +244,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} />
|
||||
<WrappedRoute path='/@:acct/:statusId/emoji_reactions' component={EmojiReactions} content={children} />
|
||||
<WrappedRoute path='/@:acct/:statusId/references' component={StatusReferences} content={children} />
|
||||
<WrappedRoute path='/@:acct/:statusId/mentioned_users' component={MentionedUsers} content={children} />
|
||||
|
||||
{/* Legacy routes, cannot be easily factored with other routes because they share a param name */}
|
||||
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
|
||||
|
@ -259,6 +262,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||
<WrappedRoute path='/antennasw' component={Antennas} content={children} />
|
||||
<WrappedRoute path='/circles/:id' component={CircleStatuses} content={children} />
|
||||
<WrappedRoute path='/circles' component={Circles} content={children} />
|
||||
|
||||
<Route component={BundleColumnError} />
|
||||
|
|
|
@ -54,6 +54,10 @@ export function Circles () {
|
|||
return import(/* webpackChunkName: "features/circles" */'../../circles');
|
||||
}
|
||||
|
||||
export function CircleStatuses () {
|
||||
return import(/* webpackChunkName: "features/circle_statuses" */'../../circle_statuses');
|
||||
}
|
||||
|
||||
export function Status () {
|
||||
return import(/* webpackChunkName: "features/status" */'../../status');
|
||||
}
|
||||
|
@ -102,6 +106,10 @@ export function StatusReferences () {
|
|||
return import(/* webpackChunkName: "features/status_references" */'../../status_references');
|
||||
}
|
||||
|
||||
export function MentionedUsers () {
|
||||
return import(/* webpackChunkName: "features/mentioned_users" */'../../mentioned_users');
|
||||
}
|
||||
|
||||
export function FollowRequests () {
|
||||
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
* @property {string} dtl_tag
|
||||
* @property {boolean} enable_emoji_reaction
|
||||
* @property {boolean} enable_login_privacy
|
||||
* @property {boolean} enable_local_privacy
|
||||
* @property {boolean} enable_dtl_menu
|
||||
* @property {boolean=} expand_spoilers
|
||||
* @property {boolean} hide_recent_emojis
|
||||
|
@ -130,6 +131,7 @@ export const displayMediaExpand = getMeta('display_media_expand');
|
|||
export const domain = getMeta('domain');
|
||||
export const dtlTag = getMeta('dtl_tag');
|
||||
export const enableEmojiReaction = getMeta('enable_emoji_reaction');
|
||||
export const enableLocalPrivacy = getMeta('enable_local_privacy');
|
||||
export const enableLoginPrivacy = getMeta('enable_login_privacy');
|
||||
export const enableDtlMenu = getMeta('enable_dtl_menu');
|
||||
export const expandSpoilers = getMeta('expand_spoilers');
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"about.blocks": "Moderated servers",
|
||||
"about.contact": "Contact:",
|
||||
"about.disabled": "Disabled",
|
||||
"about.disclaimer": "Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.",
|
||||
"about.domain_blocks.no_reason_available": "Reason not available",
|
||||
"about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.",
|
||||
|
@ -10,6 +11,8 @@
|
|||
"about.domain_blocks.silenced.title": "Limited",
|
||||
"about.domain_blocks.suspended.explanation": "No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.",
|
||||
"about.domain_blocks.suspended.title": "Suspended",
|
||||
"about.enabled": "Enabled",
|
||||
"about.kmyblue_capability": "This server is using kmyblue, a fork of Mastodon. On this server, kmyblues unique features are configured as follows.",
|
||||
"about.not_available": "This information has not been made available on this server.",
|
||||
"about.powered_by": "Decentralized social media powered by {mastodon}",
|
||||
"about.rules": "Server rules",
|
||||
|
@ -104,6 +107,8 @@
|
|||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"circles.delete": "Delete circle",
|
||||
"circles.edit": "Edit circle",
|
||||
"closed_registrations.other_server_instructions": "Since Mastodon is decentralized, you can create an account on another server and still interact with this one.",
|
||||
"closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
|
||||
"closed_registrations_modal.find_another_server": "Find another server",
|
||||
|
@ -151,6 +156,7 @@
|
|||
"compose_form.lock_disclaimer.lock": "locked",
|
||||
"compose_form.markdown.marked": "Markdown is available",
|
||||
"compose_form.markdown.unmarked": "Markdown is NOT available",
|
||||
"compose_form.mention_warning": "When you add a mention to a limited post, the person you are mentioning can also see this post.",
|
||||
"compose_form.placeholder": "What's on your mind?",
|
||||
"compose_form.searchability_warning": "Self only searchability is not available other mastodon servers. Others can search your post.",
|
||||
"compose_form.poll.add_option": "Add a choice",
|
||||
|
@ -236,6 +242,7 @@
|
|||
"empty_column.account_unavailable": "Profile unavailable",
|
||||
"empty_column.blocks": "You haven't blocked any users yet.",
|
||||
"empty_column.bookmarked_statuses": "You don't have any bookmarked posts yet. When you bookmark one, it will show up here.",
|
||||
"empty_column.circle_statuses": "You don't have any circle posts yet. When you post one as circle, it will show up here.",
|
||||
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
||||
"empty_column.direct": "You don't have any private mentions yet. When you send or receive one, it will show up here.",
|
||||
"empty_column.domain_blocks": "There are no blocked domains yet.",
|
||||
|
@ -533,6 +540,7 @@
|
|||
"privacy.login.short": "Login only",
|
||||
"privacy.mutual.long": "Mutual followers only",
|
||||
"privacy.mutual.short": "Mutual",
|
||||
"privacy.personal.short": "Yourself only",
|
||||
"privacy.private.long": "Visible for followers only",
|
||||
"privacy.private.short": "Followers only",
|
||||
"privacy.public.long": "Visible for all",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"about.blocks": "制限中のサーバー",
|
||||
"about.contact": "連絡先",
|
||||
"about.disabled": "無効",
|
||||
"about.disclaimer": "Mastodonは自由なオープンソースソフトウェアであり、Mastodon gGmbHの商標です。",
|
||||
"about.domain_blocks.no_reason_available": "理由未記載",
|
||||
"about.domain_blocks.preamble": "Mastodonでは原則的にあらゆるサーバー同士で交流したり、互いの投稿を読んだりできますが、当サーバーでは例外的に次のような制限を設けています。",
|
||||
|
@ -10,6 +11,8 @@
|
|||
"about.domain_blocks.silenced.title": "制限",
|
||||
"about.domain_blocks.suspended.explanation": "これらのサーバーからのデータは処理されず、保存や変換もされません。該当するユーザーとの交流もできません。",
|
||||
"about.domain_blocks.suspended.title": "停止中",
|
||||
"about.enabled": "有効",
|
||||
"about.kmyblue_capability": "このサーバーは、kmyblueというMastodonフォークを利用しています。kmyblue独自機能の一部は、サーバー管理者によって有効・無効を切り替えることができます。",
|
||||
"about.not_available": "この情報はこのサーバーでは利用できません。",
|
||||
"about.powered_by": "{mastodon}による分散型ソーシャルメディア",
|
||||
"about.rules": "サーバーのルール",
|
||||
|
@ -154,8 +157,10 @@
|
|||
"bundle_modal_error.close": "閉じる",
|
||||
"bundle_modal_error.message": "コンポーネントの読み込み中に問題が発生しました。",
|
||||
"bundle_modal_error.retry": "再試行",
|
||||
"circles.account.add": "おはぎに追加",
|
||||
"circles.account.remove": "おはぎから外す",
|
||||
"circles.account.add": "サークルに追加",
|
||||
"circles.account.remove": "サークルから外す",
|
||||
"circles.delete": "サークルを削除",
|
||||
"circles.edit": "サークルを編集",
|
||||
"circles.edit.submit": "タイトルを変更",
|
||||
"circles.new.create": "サークルを作成",
|
||||
"circles.new.title_placeholder": "新規サークル名",
|
||||
|
@ -213,6 +218,7 @@
|
|||
"compose_form.lock_disclaimer.lock": "承認制",
|
||||
"compose_form.markdown.marked": "Markdown有効",
|
||||
"compose_form.markdown.unmarked": "Markdownは有効になっていません",
|
||||
"compose_form.mention_warning": "限定投稿にメンションを追加すると、そのアカウントはサークルメンバー・相互などに関係なくこの投稿を読むことができます",
|
||||
"compose_form.placeholder": "今なにしてる?",
|
||||
"compose_form.searchability_warning": "検索許可「自分のみ」はkmyblue内の検索でのみ有効です。他のサーバーでは「リアクションした人のみ」と同等に扱われます",
|
||||
"compose_form.poll.add_option": "追加",
|
||||
|
@ -308,6 +314,7 @@
|
|||
"empty_column.bookmark_categories": "まだ分類がありません。分類を作るとここに表示されます。",
|
||||
"empty_column.bookmarked_statuses": "まだ何もブックマーク登録していません。ブックマーク登録するとここに表示されます。",
|
||||
"empty_column.circles": "まだサークルがありません。サークルを作るとここに表示されます。",
|
||||
"empty_column.circle_statuses": "まだサークル投稿がありません。このサークルでなにか投稿するとここに表示されます。",
|
||||
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
|
||||
"empty_column.direct": "非公開の返信はまだありません。非公開でやりとりをするとここに表示されます。",
|
||||
"empty_column.domain_blocks": "ブロックしているドメインはありません。",
|
||||
|
@ -618,6 +625,7 @@
|
|||
"privacy.login.short": "ログインユーザーのみ",
|
||||
"privacy.mutual.long": "相互フォローさんのみ閲覧可、限定投稿",
|
||||
"privacy.mutual.short": "相互のみ",
|
||||
"privacy.personal.short": "自分限定",
|
||||
"privacy.private.long": "フォロワーのみ閲覧可",
|
||||
"privacy.private.short": "フォロワーのみ",
|
||||
"privacy.public.long": "誰でも閲覧可、ホーム+ローカル+連合TL",
|
||||
|
|
|
@ -48,11 +48,11 @@
|
|||
"account.media": "Médiá",
|
||||
"account.mention": "Spomeň @{name}",
|
||||
"account.moved_to": "{name} uvádza, že jeho/jej nový účet je teraz:",
|
||||
"account.mute": "Stíš @{name}",
|
||||
"account.mute_notifications_short": "Stíš oznámenia",
|
||||
"account.mute_short": "Stíš",
|
||||
"account.muted": "Stíšený",
|
||||
"account.no_bio": "Nie je uvedený žiadny popis.",
|
||||
"account.mute": "Nevšímaj si @{name}",
|
||||
"account.mute_notifications_short": "Stíš oboznámenia",
|
||||
"account.mute_short": "Nevšímaj si",
|
||||
"account.muted": "Nevšímaný/á",
|
||||
"account.no_bio": "Nieje uvedený žiadny popis.",
|
||||
"account.open_original_page": "Otvor pôvodnú stránku",
|
||||
"account.posts": "Príspevky",
|
||||
"account.posts_with_replies": "Príspevky a odpovede",
|
||||
|
@ -307,9 +307,8 @@
|
|||
"home.column_settings.basic": "Základné",
|
||||
"home.column_settings.show_reblogs": "Ukáž vyzdvihnuté",
|
||||
"home.column_settings.show_replies": "Ukáž odpovede",
|
||||
"home.explore_prompt.body": "Váš domovský informačný kanál bude obsahovať mix príspevkov z mriežok, ktoré ste sa rozhodli sledovať, ľudí, ktorých ste sa rozhodli sledovať, a príspevkov, ktoré preferujú. Ak sa vám to zdá príliš málo, možno budete chcieť:",
|
||||
"home.explore_prompt.title": "Toto je tvoja domovina v rámci Mastodonu.",
|
||||
"home.hide_announcements": "Skry oznámenia",
|
||||
"home.hide_announcements": "Skry oboznámenia",
|
||||
"home.pending_critical_update.body": "Prosím aktualizuj si svoj Mastodon server, ako náhle to bude možné!",
|
||||
"home.pending_critical_update.link": "Pozri aktualizácie",
|
||||
"home.pending_critical_update.title": "Je dostupná kritická bezpečnostná aktualizácia!",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { List as ImmutableList, fromJS } from 'immutable';
|
||||
import { List as ImmutableList, Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
CIRCLE_FETCH_SUCCESS,
|
||||
|
@ -7,31 +7,107 @@ import {
|
|||
CIRCLE_CREATE_SUCCESS,
|
||||
CIRCLE_UPDATE_SUCCESS,
|
||||
CIRCLE_DELETE_SUCCESS,
|
||||
CIRCLE_STATUSES_FETCH_REQUEST,
|
||||
CIRCLE_STATUSES_FETCH_SUCCESS,
|
||||
CIRCLE_STATUSES_FETCH_FAIL,
|
||||
CIRCLE_STATUSES_EXPAND_REQUEST,
|
||||
CIRCLE_STATUSES_EXPAND_SUCCESS,
|
||||
CIRCLE_STATUSES_EXPAND_FAIL,
|
||||
} from '../actions/circles';
|
||||
import {
|
||||
COMPOSE_WITH_CIRCLE_SUCCESS,
|
||||
} from '../actions/compose';
|
||||
|
||||
const initialState = ImmutableList();
|
||||
|
||||
const normalizeList = (state, circle) => state.set(circle.id, fromJS(circle));
|
||||
const initialStatusesState = ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
isLoading: false,
|
||||
loaded: true,
|
||||
next: null,
|
||||
});
|
||||
|
||||
const normalizeLists = (state, circles) => {
|
||||
const normalizeCircle = (state, circle) => {
|
||||
const old = state.get(circle.id);
|
||||
if (old === false) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let s = state.set(circle.id, fromJS(circle));
|
||||
if (old) {
|
||||
s = s.setIn([circle.id, 'statuses'], old.get('statuses'));
|
||||
} else {
|
||||
s = s.setIn([circle.id, 'statuses'], initialStatusesState);
|
||||
}
|
||||
return s.setIn([circle.id, 'isLoading'], false).setIn([circle.id, 'isLoaded'], true);
|
||||
};
|
||||
|
||||
const normalizeCircles = (state, circles) => {
|
||||
circles.forEach(circle => {
|
||||
state = normalizeList(state, circle);
|
||||
state = normalizeCircle(state, circle);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const normalizeCircleStatuses = (state, circleId, statuses, next) => {
|
||||
return state.updateIn([circleId, 'statuses'], listMap => listMap.withMutations(map => {
|
||||
map.set('next', next);
|
||||
map.set('loaded', true);
|
||||
map.set('isLoading', false);
|
||||
map.set('items', ImmutableOrderedSet(statuses.map(item => item.id)));
|
||||
}));
|
||||
};
|
||||
|
||||
const appendToCircleStatuses = (state, circleId, statuses, next) => {
|
||||
return appendToCircleStatusesById(state, circleId, statuses.map(item => item.id), next);
|
||||
};
|
||||
|
||||
const appendToCircleStatusesById = (state, circleId, statuses, next) => {
|
||||
return state.updateIn([circleId, 'statuses'], listMap => listMap.withMutations(map => {
|
||||
if (typeof next !== 'undefined') {
|
||||
map.set('next', next);
|
||||
}
|
||||
map.set('isLoading', false);
|
||||
if (map.get('items')) {
|
||||
map.set('items', map.get('items').union(statuses));
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const prependToCircleStatusById = (state, circleId, statusId) => {
|
||||
if (!state.get(circleId)) return state;
|
||||
|
||||
return state.updateIn([circleId], circle => circle.withMutations(map => {
|
||||
if (map.getIn(['statuses', 'items'])) {
|
||||
map.updateIn(['statuses', 'items'], list => ImmutableOrderedSet([statusId]).union(list));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
export default function circles(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case CIRCLE_FETCH_SUCCESS:
|
||||
case CIRCLE_CREATE_SUCCESS:
|
||||
case CIRCLE_UPDATE_SUCCESS:
|
||||
return normalizeList(state, action.circle);
|
||||
return normalizeCircle(state, action.circle);
|
||||
case CIRCLES_FETCH_SUCCESS:
|
||||
return normalizeLists(state, action.circles);
|
||||
return normalizeCircles(state, action.circles);
|
||||
case CIRCLE_DELETE_SUCCESS:
|
||||
case CIRCLE_FETCH_FAIL:
|
||||
return state.set(action.id, false);
|
||||
case CIRCLE_STATUSES_FETCH_REQUEST:
|
||||
case CIRCLE_STATUSES_EXPAND_REQUEST:
|
||||
return state.setIn([action.id, 'statuses', 'isLoading'], true);
|
||||
case CIRCLE_STATUSES_FETCH_FAIL:
|
||||
case CIRCLE_STATUSES_EXPAND_FAIL:
|
||||
return state.setIn([action.id, 'statuses', 'isLoading'], false);
|
||||
case CIRCLE_STATUSES_FETCH_SUCCESS:
|
||||
return normalizeCircleStatuses(state, action.id, action.statuses, action.next);
|
||||
case CIRCLE_STATUSES_EXPAND_SUCCESS:
|
||||
return appendToCircleStatuses(state, action.id, action.statuses, action.next);
|
||||
case COMPOSE_WITH_CIRCLE_SUCCESS:
|
||||
return prependToCircleStatusById(state, action.circleId, action.status.id);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ import {
|
|||
import { REDRAFT } from '../actions/statuses';
|
||||
import { STORE_HYDRATE } from '../actions/store';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { me } from '../initial_state';
|
||||
import { enableLocalPrivacy, enableLoginPrivacy, me } from '../initial_state';
|
||||
import { unescapeHTML } from '../utils/html';
|
||||
import { uuid } from '../uuid';
|
||||
|
||||
|
@ -138,9 +138,13 @@ function clearAll(state) {
|
|||
if (state.get('stay_privacy') && !state.get('in_reply_to')) {
|
||||
map.set('default_privacy', state.get('privacy'));
|
||||
}
|
||||
if ((map.get('privacy') === 'login' && !enableLoginPrivacy) || (map.get('privacy') === 'public_unlisted' && !enableLocalPrivacy)) {
|
||||
map.set('privacy', 'public');
|
||||
}
|
||||
if (!state.get('in_reply_to')) {
|
||||
map.set('posted_on_this_session', true);
|
||||
}
|
||||
map.set('limited_scope', null);
|
||||
map.set('id', null);
|
||||
map.set('in_reply_to', null);
|
||||
map.set('searchability', state.get('default_searchability'));
|
||||
|
@ -408,6 +412,7 @@ export default function compose(state = initialState, action) {
|
|||
map.set('in_reply_to', action.status.get('id'));
|
||||
map.set('text', statusToTextMentions(state, action.status));
|
||||
map.set('privacy', privacyPreference(action.status.get('visibility_ex'), state.get('default_privacy')));
|
||||
map.set('limited_scope', null);
|
||||
map.set('searchability', privacyPreference(action.status.get('searchability'), state.get('default_searchability')));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
|
@ -544,6 +549,7 @@ export default function compose(state = initialState, action) {
|
|||
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
|
||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||
map.set('privacy', action.status.get('visibility_ex'));
|
||||
map.set('limited_scope', null);
|
||||
map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
|
@ -574,7 +580,12 @@ export default function compose(state = initialState, action) {
|
|||
map.set('id', action.status.get('id'));
|
||||
map.set('text', action.text);
|
||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||
map.set('privacy', action.status.get('visibility_ex'));
|
||||
if (action.status.get('visibility_ex') !== 'limited') {
|
||||
map.set('privacy', action.status.get('visibility_ex'));
|
||||
} else {
|
||||
map.set('privacy', action.status.get('limited_scope') === 'mutual' ? 'mutual' : 'circle');
|
||||
}
|
||||
map.set('limited_scope', action.status.get('limited_scope'));
|
||||
map.set('media_attachments', action.status.get('media_attachments'));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import Immutable from 'immutable';
|
||||
|
||||
import {
|
||||
DROPDOWN_MENU_OPEN,
|
||||
DROPDOWN_MENU_CLOSE,
|
||||
} from '../actions/dropdown_menu';
|
||||
|
||||
const initialState = Immutable.Map({ openId: null, keyboard: false, scroll_key: null });
|
||||
|
||||
export default function dropdownMenu(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case DROPDOWN_MENU_OPEN:
|
||||
return state.merge({ openId: action.id, keyboard: action.keyboard, scroll_key: action.scroll_key });
|
||||
case DROPDOWN_MENU_CLOSE:
|
||||
return state.get('openId') === action.id ? state.set('openId', null).set('scroll_key', null) : state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
33
app/javascript/mastodon/reducers/dropdown_menu.ts
Normal file
33
app/javascript/mastodon/reducers/dropdown_menu.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
import { closeDropdownMenu, openDropdownMenu } from '../actions/dropdown_menu';
|
||||
|
||||
interface DropdownMenuState {
|
||||
openId: string | null;
|
||||
keyboard: boolean;
|
||||
scrollKey: string | null;
|
||||
}
|
||||
|
||||
const initialState: DropdownMenuState = {
|
||||
openId: null,
|
||||
keyboard: false,
|
||||
scrollKey: null,
|
||||
};
|
||||
|
||||
export const dropdownMenuReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(
|
||||
openDropdownMenu,
|
||||
(state, { payload: { id, keyboard, scrollKey } }) => {
|
||||
state.openId = id;
|
||||
state.keyboard = keyboard;
|
||||
state.scrollKey = scrollKey;
|
||||
},
|
||||
)
|
||||
.addCase(closeDropdownMenu, (state, { payload: { id } }) => {
|
||||
if (state.openId === id) {
|
||||
state.openId = null;
|
||||
state.scrollKey = null;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -24,7 +24,7 @@ import contexts from './contexts';
|
|||
import conversations from './conversations';
|
||||
import custom_emojis from './custom_emojis';
|
||||
import domain_lists from './domain_lists';
|
||||
import dropdown_menu from './dropdown_menu';
|
||||
import { dropdownMenuReducer } from './dropdown_menu';
|
||||
import filters from './filters';
|
||||
import followed_tags from './followed_tags';
|
||||
import height_cache from './height_cache';
|
||||
|
@ -56,7 +56,7 @@ import user_lists from './user_lists';
|
|||
|
||||
const reducers = {
|
||||
announcements,
|
||||
dropdown_menu,
|
||||
dropdownMenu: dropdownMenuReducer,
|
||||
timelines,
|
||||
meta,
|
||||
alerts,
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Record as ImmutableRecord, Stack } from 'immutable';
|
||||
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { Reducer } from '@reduxjs/toolkit';
|
||||
|
||||
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
|
||||
import type { ModalType } from '../actions/modal';
|
||||
import { openModal, closeModal } from '../actions/modal';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
|
||||
type ModalProps = Record<string, unknown>;
|
||||
export type ModalProps = Record<string, unknown>;
|
||||
interface Modal {
|
||||
modalType: ModalType;
|
||||
modalProps: ModalProps;
|
||||
|
@ -62,33 +62,22 @@ const pushModal = (
|
|||
});
|
||||
};
|
||||
|
||||
export function modalReducer(
|
||||
state: State = initialState,
|
||||
action: PayloadAction<{
|
||||
modalType: ModalType;
|
||||
ignoreFocus: boolean;
|
||||
modalProps: Record<string, unknown>;
|
||||
}>,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case openModal.type:
|
||||
return pushModal(
|
||||
state,
|
||||
action.payload.modalType,
|
||||
action.payload.modalProps,
|
||||
);
|
||||
case closeModal.type:
|
||||
return popModal(state, action.payload);
|
||||
case COMPOSE_UPLOAD_CHANGE_SUCCESS:
|
||||
return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
|
||||
case TIMELINE_DELETE:
|
||||
return state.update('stack', (stack) =>
|
||||
stack.filterNot(
|
||||
// @ts-expect-error TIMELINE_DELETE action is not typed yet.
|
||||
(modal) => modal.get('modalProps').statusId === action.id,
|
||||
),
|
||||
);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
export const modalReducer: Reducer<State> = (state = initialState, action) => {
|
||||
if (openModal.match(action))
|
||||
return pushModal(
|
||||
state,
|
||||
action.payload.modalType,
|
||||
action.payload.modalProps,
|
||||
);
|
||||
else if (closeModal.match(action)) return popModal(state, action.payload);
|
||||
// TODO: type those actions
|
||||
else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS)
|
||||
return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
|
||||
else if (action.type === TIMELINE_DELETE)
|
||||
return state.update('stack', (stack) =>
|
||||
stack.filterNot(
|
||||
(modal) => modal.get('modalProps').statusId === action.id,
|
||||
),
|
||||
);
|
||||
else return state;
|
||||
};
|
||||
|
|
|
@ -64,6 +64,12 @@ import {
|
|||
EMOJI_REACTIONS_EXPAND_SUCCESS,
|
||||
EMOJI_REACTIONS_EXPAND_FAIL,
|
||||
STATUS_REFERENCES_FETCH_SUCCESS,
|
||||
MENTIONED_USERS_FETCH_REQUEST,
|
||||
MENTIONED_USERS_FETCH_SUCCESS,
|
||||
MENTIONED_USERS_FETCH_FAIL,
|
||||
MENTIONED_USERS_EXPAND_REQUEST,
|
||||
MENTIONED_USERS_EXPAND_SUCCESS,
|
||||
MENTIONED_USERS_EXPAND_FAIL,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
MUTES_FETCH_REQUEST,
|
||||
|
@ -92,6 +98,7 @@ const initialState = ImmutableMap({
|
|||
favourited_by: initialListState,
|
||||
emoji_reactioned_by: initialListState,
|
||||
referred_by: initialListState,
|
||||
mentioned_users: initialListState,
|
||||
follow_requests: initialListState,
|
||||
blocks: initialListState,
|
||||
mutes: initialListState,
|
||||
|
@ -205,6 +212,16 @@ export default function userLists(state = initialState, action) {
|
|||
return appendToEmojiReactionList(state, ['emoji_reactioned_by', action.id], action.accounts, action.next);
|
||||
case STATUS_REFERENCES_FETCH_SUCCESS:
|
||||
return state.setIn(['referred_by', action.id], ImmutableList(action.statuses.map(item => item.id)));
|
||||
case MENTIONED_USERS_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['mentioned_users', action.id], action.accounts, action.next);
|
||||
case MENTIONED_USERS_EXPAND_SUCCESS:
|
||||
return appendToList(state, ['mentioned_users', action.id], action.accounts, action.next);
|
||||
case MENTIONED_USERS_FETCH_REQUEST:
|
||||
case MENTIONED_USERS_EXPAND_REQUEST:
|
||||
return state.setIn(['mentioned_users', action.id, 'isLoading'], true);
|
||||
case MENTIONED_USERS_FETCH_FAIL:
|
||||
case MENTIONED_USERS_EXPAND_FAIL:
|
||||
return state.setIn(['mentioned_users', action.id, 'isLoading'], false);
|
||||
case NOTIFICATIONS_UPDATE:
|
||||
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||
|
|
|
@ -135,3 +135,7 @@ export const getStatusList = createSelector([
|
|||
export const getBookmarkCategoryStatusList = createSelector([
|
||||
(state, bookmarkCategoryId) => state.getIn(['bookmark_categories', bookmarkCategoryId, 'items']),
|
||||
], (items) => items ? items.toList() : ImmutableList());
|
||||
|
||||
export const getCircleStatusList = createSelector([
|
||||
(state, circleId) => state.getIn(['circles', circleId, 'statuses', 'items']),
|
||||
], (items) => items ? items.toList() : ImmutableList());
|
||||
|
|
29
app/javascript/mastodon/utils/mentions.ts
Normal file
29
app/javascript/mastodon/utils/mentions.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
const MENTION_SEPARATORS = '_\\u00b7\\u200c';
|
||||
const ALPHA = '\\p{L}\\p{M}';
|
||||
const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
|
||||
|
||||
const buildMentionPatternRegex = () => {
|
||||
try {
|
||||
return new RegExp(
|
||||
`(?:^|[^\\/\\)\\w])@(([${WORD}_][${WORD}${MENTION_SEPARATORS}]*[${ALPHA}${MENTION_SEPARATORS}][${WORD}${MENTION_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))`,
|
||||
'iu',
|
||||
);
|
||||
} catch {
|
||||
return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
|
||||
}
|
||||
};
|
||||
|
||||
const buildMentionRegex = () => {
|
||||
try {
|
||||
return new RegExp(
|
||||
`^(([${WORD}_][${WORD}${MENTION_SEPARATORS}]*[${ALPHA}${MENTION_SEPARATORS}][${WORD}${MENTION_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))$`,
|
||||
'iu',
|
||||
);
|
||||
} catch {
|
||||
return /^(\w*[a-zA-Z·]\w*)$/i;
|
||||
}
|
||||
};
|
||||
|
||||
export const MENTION_PATTERN_REGEX = buildMentionPatternRegex();
|
||||
|
||||
export const MENTION_REGEX = buildMentionRegex();
|
|
@ -284,6 +284,7 @@
|
|||
font-size: 11px;
|
||||
padding: 0 3px;
|
||||
line-height: 27px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue