Merge commit '6b519cfefa93a923b19d0f20c292c7185f8fd5f5' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2025-02-27 15:52:20 +01:00
60 changed files with 1394 additions and 917 deletions

View File

@@ -142,6 +142,13 @@ export function fetchAccountFail(id, error) {
};
}
/**
* @param {string} id
* @param {Object} options
* @param {boolean} [options.reblogs]
* @param {boolean} [options.notify]
* @returns {function(): void}
*/
export function followAccount(id, options = { reblogs: true }) {
return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);

View File

@@ -55,7 +55,7 @@ export const FollowButton: React.FC<{
);
}
if (!relationship) return;
if (!relationship || !accountId) return;
if (accountId === me) {
return;

View File

@@ -1,528 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { NavLink, withRouter } from 'react-router-dom';
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
import { CopyIconButton } from 'mastodon/components/copy_icon_button';
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import AccountNoteContainer from '../containers/account_note_container';
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
import { DomainPill } from './domain_pill';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' },
media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
});
const titleFromAccount = account => {
const displayName = account.get('display_name');
const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${localDomain}` : account.get('acct');
const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
return `${prefix} (@${acct})`;
};
const messageForFollowButton = relationship => {
if(!relationship) return messages.follow;
if (relationship.get('following') && relationship.get('followed_by')) {
return messages.mutual;
} else if (relationship.get('following') || relationship.get('requested')) {
return messages.unfollow;
} else if (relationship.get('followed_by')) {
return messages.followBack;
} else {
return messages.follow;
}
};
const dateFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
};
class Header extends ImmutablePureComponent {
static propTypes = {
identity: identityContextPropShape,
account: ImmutablePropTypes.record,
identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired,
onNotifyToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
onChangeLanguages: PropTypes.func.isRequired,
onInteractionModal: PropTypes.func.isRequired,
onOpenAvatar: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
...WithRouterPropTypes,
};
setRef = c => {
this.node = c;
};
openEditProfile = () => {
window.open('/settings/profile', '_blank');
};
isStatusesPageActive = (match, location) => {
if (!match) {
return false;
}
return !location.pathname.match(/\/(followers|following)\/?$/);
};
handleMouseEnter = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
};
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
};
handleAvatarClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.props.onOpenAvatar();
}
};
handleShare = () => {
const { account } = this.props;
navigator.share({
url: account.get('url'),
}).catch((e) => {
if (e.name !== 'AbortError') console.error(e);
});
};
handleHashtagClick = e => {
const { history } = this.props;
const value = e.currentTarget.textContent.replace(/^#/, '');
if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
history.push(`/tags/${value}`);
}
};
handleMentionClick = e => {
const { history, onOpenURL } = this.props;
if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
const link = e.currentTarget;
onOpenURL(link.href).then((result) => {
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`);
} else {
window.location = link.href;
}
} else if (isRejected(result)) {
window.location = link.href;
}
}).catch(() => {
// Nothing
});
}
};
_attachLinkEvents () {
const node = this.node;
if (!node) {
return;
}
const links = node.querySelectorAll('a');
let link;
for (var i = 0; i < links.length; ++i) {
link = links[i];
if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.handleHashtagClick, false);
} else if (link.classList.contains('mention')) {
link.addEventListener('click', this.handleMentionClick, false);
}
}
}
componentDidMount () {
this._attachLinkEvents();
}
componentDidUpdate () {
this._attachLinkEvents();
}
render () {
const { account, hidden, intl } = this.props;
const { signedIn, permissions } = this.props.identity;
if (!account) {
return null;
}
const suspended = account.get('suspended');
const isRemote = account.get('acct') !== account.get('username');
const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null;
let actionBtn, bellBtn, lockedIcon, shareBtn;
let info = [];
let menu = [];
if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
info.push(<span key='blocked' className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
}
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
info.push(<span key='muted' className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
}
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} iconComponent={account.getIn(['relationship', 'notifying']) ? NotificationsActiveIcon : NotificationsIcon} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
}
if ('share' in navigator) {
shareBtn = <IconButton className='optional' iconComponent={ShareIcon} title={intl.formatMessage(messages.share, { name: account.get('username') })} onClick={this.handleShare} />;
} else {
shareBtn = <CopyIconButton className='optional' title={intl.formatMessage(messages.copy)} value={account.get('url')} />;
}
if (me !== account.get('id')) {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = <Button disabled><LoadingIndicator /></Button>;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
} else {
actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
}
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
actionBtn = '';
}
if (account.get('locked')) {
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
}
if (signedIn && account.get('id') !== me && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
}
if (isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
menu.push(null);
}
if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
} else if (signedIn) {
if (account.getIn(['relationship', 'following'])) {
if (!account.getIn(['relationship', 'muting'])) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
} else {
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
}
menu.push({ text: intl.formatMessage(messages.languages), action: this.props.onChangeLanguages });
menu.push(null);
}
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
menu.push(null);
}
if (account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true });
}
if (account.getIn(['relationship', 'blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
}
if (!account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
}
}
if (signedIn && isRemote) {
menu.push(null);
if (account.getIn(['relationship', 'domain_blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true });
}
}
if ((account.get('id') !== me && (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null);
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
}
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain }), href: `/admin/instances/${remoteDomain}` });
}
}
const content = { __html: account.get('note_emojified') };
const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields');
const isLocal = account.get('acct').indexOf('@') === -1;
const username = account.get('acct').split('@')[0];
const domain = isLocal ? localDomain : account.get('acct').split('@')[1];
const isIndexable = !account.get('noindex');
const badges = [];
if (account.get('bot')) {
badges.push(<AutomatedBadge key='bot-badge' />);
} else if (account.get('group')) {
badges.push(<GroupBadge key='group-badge' />);
}
account.get('roles', []).forEach((role) => {
badges.push(<Badge key={`role-badge-${role.get('id')}`} label={<span>{role.get('name')}</span>} domain={domain} roleId={role.get('id')} />);
});
return (
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
{!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />}
<div className='account__header__image'>
<div className='account__header__info'>
{info}
</div>
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
</div>
<div className='account__header__bar'>
<div className='account__header__tabs'>
<a className='avatar' href={account.get('avatar')} rel='noopener' target='_blank' onClick={this.handleAvatarClick}>
<Avatar account={suspended || hidden ? undefined : account} size={90} />
</a>
<div className='account__header__tabs__buttons'>
{!hidden && bellBtn}
{!hidden && shareBtn}
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
{!hidden && actionBtn}
</div>
</div>
<div className='account__header__tabs__name'>
<h1>
<span dangerouslySetInnerHTML={displayNameHtml} />
<small>
<span>@{username}<span className='invisible'>@{domain}</span></span>
<DomainPill username={username} domain={domain} isSelf={me === account.get('id')} />
{lockedIcon}
</small>
</h1>
</div>
{badges.length > 0 && (
<div className='account__header__badges'>
{badges}
</div>
)}
{!(suspended || hidden) && (
<div className='account__header__extra'>
<div className='account__header__bio' ref={this.setRef}>
{(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
<div className='account__header__fields'>
<dl>
<dt><FormattedMessage id='account.joined_short' defaultMessage='Joined' /></dt>
<dd>{intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' })}</dd>
</dl>
{fields.map((pair, i) => (
<dl key={i} className={classNames({ verified: pair.get('verified_at') })}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
<dd className='translate' title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' icon={CheckIcon} className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl>
))}
</div>
</div>
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber
value={account.get('statuses_count')}
renderer={StatusesCounter}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<ShortNumber
value={account.get('following_count')}
renderer={FollowingCounter}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<ShortNumber
value={account.get('followers_count')}
renderer={FollowersCounter}
/>
</NavLink>
</div>
</div>
)}
</div>
<Helmet>
<title>{titleFromAccount(account)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={account.get('url')} />
</Helmet>
</div>
);
}
}
export default withRouter(withIdentity(injectIntl(Header)));

View File

@@ -17,7 +17,7 @@ import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountGallery } from 'mastodon/selectors';
import { expandAccountMediaTimeline } from '../../actions/timelines';
import HeaderContainer from '../account_timeline/containers/header_container';
import { AccountHeader } from '../account_timeline/components/account_header';
import Column from '../ui/components/column';
import { MediaItem } from './components/media_item';
@@ -207,7 +207,7 @@ class AccountGallery extends ImmutablePureComponent {
<ScrollContainer scrollKey='account_gallery'>
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.accountId} />
<AccountHeader accountId={this.props.accountId} />
{(suspended || blockedBy) ? (
<div className='empty-column-indicator'>

File diff suppressed because it is too large Load Diff

View File

@@ -1,155 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import InnerHeader from '../../account/components/header';
import MemorialNote from './memorial_note';
import MovedNote from './moved_note';
class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.record,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onChangeLanguages: PropTypes.func.isRequired,
onInteractionModal: PropTypes.func.isRequired,
onOpenAvatar: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
};
handleBlock = () => {
this.props.onBlock(this.props.account);
};
handleMention = () => {
this.props.onMention(this.props.account);
};
handleDirect = () => {
this.props.onDirect(this.props.account);
};
handleReport = () => {
this.props.onReport(this.props.account);
};
handleReblogToggle = () => {
this.props.onReblogToggle(this.props.account);
};
handleNotifyToggle = () => {
this.props.onNotifyToggle(this.props.account);
};
handleMute = () => {
this.props.onMute(this.props.account);
};
handleBlockDomain = () => {
this.props.onBlockDomain(this.props.account);
};
handleUnblockDomain = () => {
const domain = this.props.account.get('acct').split('@')[1];
if (!domain) return;
this.props.onUnblockDomain(domain);
};
handleEndorseToggle = () => {
this.props.onEndorseToggle(this.props.account);
};
handleAddToList = () => {
this.props.onAddToList(this.props.account);
};
handleEditAccountNote = () => {
this.props.onEditAccountNote(this.props.account);
};
handleChangeLanguages = () => {
this.props.onChangeLanguages(this.props.account);
};
handleInteractionModal = () => {
this.props.onInteractionModal(this.props.account);
};
handleOpenAvatar = () => {
this.props.onOpenAvatar(this.props.account);
};
render () {
const { account, hidden, hideTabs } = this.props;
if (account === null) {
return null;
}
return (
<div className='account-timeline__header'>
{(!hidden && account.get('memorial')) && <MemorialNote />}
{(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
<InnerHeader
account={account}
onFollow={this.handleFollow}
onBlock={this.handleBlock}
onMention={this.handleMention}
onDirect={this.handleDirect}
onReblogToggle={this.handleReblogToggle}
onNotifyToggle={this.handleNotifyToggle}
onReport={this.handleReport}
onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
onEditAccountNote={this.handleEditAccountNote}
onChangeLanguages={this.handleChangeLanguages}
onInteractionModal={this.handleInteractionModal}
onOpenAvatar={this.handleOpenAvatar}
onOpenURL={this.props.onOpenURL}
domain={this.props.domain}
hidden={hidden}
/>
{!(hideTabs || hidden) && (
<div className='account__section-headline'>
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
<NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div>
)}
</div>
);
}
}
export default Header;

View File

@@ -1,153 +0,0 @@
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openURL } from 'mastodon/actions/search';
import {
followAccount,
unblockAccount,
unmuteAccount,
pinAccount,
unpinAccount,
} from '../../../actions/accounts';
import { initBlockModal } from '../../../actions/blocks';
import {
mentionCompose,
directCompose,
} from '../../../actions/compose';
import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
import { makeGetAccount, getAccountHidden } from '../../../selectors';
import Header from '../components/header';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
domain: state.getIn(['meta', 'domain']),
hidden: getAccountHidden(state, accountId),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
} else {
dispatch(followAccount(account.get('id')));
}
},
onInteractionModal (account) {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'follow',
accountId: account.get('id'),
url: account.get('uri'),
},
}));
},
onBlock (account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
} else {
dispatch(initBlockModal(account));
}
},
onMention (account) {
dispatch(mentionCompose(account));
},
onDirect (account) {
dispatch(directCompose(account));
},
onReblogToggle (account) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
dispatch(followAccount(account.get('id'), { reblogs: false }));
} else {
dispatch(followAccount(account.get('id'), { reblogs: true }));
}
},
onEndorseToggle (account) {
if (account.getIn(['relationship', 'endorsed'])) {
dispatch(unpinAccount(account.get('id')));
} else {
dispatch(pinAccount(account.get('id')));
}
},
onNotifyToggle (account) {
if (account.getIn(['relationship', 'notifying'])) {
dispatch(followAccount(account.get('id'), { notify: false }));
} else {
dispatch(followAccount(account.get('id'), { notify: true }));
}
},
onReport (account) {
dispatch(initReport(account));
},
onMute (account) {
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(initMuteModal(account));
}
},
onBlockDomain (account) {
dispatch(initDomainBlockModal(account));
},
onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
},
onAddToList (account) {
dispatch(openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: account.get('id'),
},
}));
},
onChangeLanguages (account) {
dispatch(openModal({
modalType: 'SUBSCRIBED_LANGUAGES',
modalProps: {
accountId: account.get('id'),
},
}));
},
onOpenAvatar (account) {
dispatch(openModal({
modalType: 'IMAGE',
modalProps: {
src: account.get('avatar'),
alt: '',
},
}));
},
onOpenURL (url) {
return dispatch(openURL({ url }));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));

View File

@@ -11,7 +11,7 @@ import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { me } from 'mastodon/initial_state';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
import { lookupAccount, fetchAccount } from '../../actions/accounts';
@@ -22,8 +22,8 @@ import { LoadingIndicator } from '../../components/loading_indicator';
import StatusList from '../../components/status_list';
import Column from '../ui/components/column';
import { AccountHeader } from './components/account_header';
import { LimitedAccountHint } from './components/limited_account_hint';
import HeaderContainer from './containers/header_container';
const emptyList = ImmutableList();
@@ -198,7 +198,7 @@ class AccountTimeline extends ImmutablePureComponent {
<ColumnBackButton />
<StatusList
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
alwaysPrepend
append={remoteMessage}
scrollKey='account_timeline'

View File

@@ -10,9 +10,10 @@ import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
import {
@@ -25,7 +26,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => {
@@ -168,7 +168,7 @@ class Followers extends ImmutablePureComponent {
hasMore={!forceEmptyState && hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
alwaysPrepend
append={remoteMessage}
emptyMessage={emptyMessage}

View File

@@ -10,9 +10,10 @@ import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
import {
@@ -25,7 +26,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => {
@@ -168,7 +168,7 @@ class Following extends ImmutablePureComponent {
hasMore={!forceEmptyState && hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
alwaysPrepend
append={remoteMessage}
emptyMessage={emptyMessage}

View File

@@ -696,6 +696,7 @@
"poll_button.remove_poll": "Elimina l'enquesta",
"privacy.change": "Canvia la privacitat del tut",
"privacy.direct.long": "Tothom mencionat a la publicació",
"privacy.direct.short": "Menció privada",
"privacy.private.long": "Només els vostres seguidors",
"privacy.private.short": "Seguidors",
"privacy.public.long": "Tothom dins o fora Mastodon",

View File

@@ -192,7 +192,7 @@
"compose_form.poll.switch_to_multiple": "Cambiar la encuesta para permitir múltiples opciones",
"compose_form.poll.switch_to_single": "Cambiar la encuesta para permitir una única opción",
"compose_form.poll.type": "Estilo",
"compose_form.publish": "Publicación",
"compose_form.publish": "Publicar",
"compose_form.publish_form": "Nueva publicación",
"compose_form.reply": "Respuesta",
"compose_form.save_changes": "Actualización",

View File

@@ -697,6 +697,7 @@
"poll_button.remove_poll": "Bain suirbhé",
"privacy.change": "Athraigh príobháideacht postála",
"privacy.direct.long": "Luaigh gach duine sa phost",
"privacy.direct.short": "Tagairt phríobháideach",
"privacy.private.long": "Do leanúna amháin",
"privacy.private.short": "Leantóirí",
"privacy.public.long": "Duine ar bith ar agus amach Mastodon",

View File

@@ -702,7 +702,7 @@
"privacy.private.short": "Follower",
"privacy.public.long": "Chiunque dentro e fuori Mastodon",
"privacy.public.short": "Pubblico",
"privacy.unlisted.additional": "",
"privacy.unlisted.additional": "Si comporta esattamente come pubblico, tranne per il fatto che il post non verrà visualizzato nei feed live o negli hashtag, nell'esplorazione o nella ricerca Mastodon, anche se hai attivato l'attivazione a livello di account.",
"privacy.unlisted.long": "Meno fanfare algoritmiche",
"privacy.unlisted.short": "Pubblico silenzioso",
"privacy_policy.last_updated": "Ultimo aggiornamento {date}",

View File

@@ -86,6 +86,12 @@
"alert.unexpected.message": "Radās negaidīta kļūda.",
"alert.unexpected.title": "Ups!",
"alt_text_badge.title": "Alt teksts",
"alt_text_modal.add_text_from_image": "Pievienot tekstu no attēla",
"alt_text_modal.cancel": "Atcelt",
"alt_text_modal.change_thumbnail": "Nomainīt sīktēlu",
"alt_text_modal.describe_for_people_with_hearing_impairments": "Aprakstīt šo cilvēkiem ar dzirdes traucējumiem…",
"alt_text_modal.describe_for_people_with_visual_impairments": "Aprakstīt šo cilvēkiem ar redzes traucējumiem…",
"alt_text_modal.done": "Gatavs",
"announcement.announcement": "Paziņojums",
"annual_report.summary.archetype.oracle": "Orākuls",
"annual_report.summary.archetype.replier": "Sabiedriskais tauriņš",
@@ -205,6 +211,7 @@
"confirmations.logout.confirm": "Iziet",
"confirmations.logout.message": "Vai tiešām vēlies izrakstīties?",
"confirmations.logout.title": "Atteikties?",
"confirmations.missing_alt_text.secondary": "Vienalga iesūtīt",
"confirmations.mute.confirm": "Apklusināt",
"confirmations.redraft.confirm": "Dzēst un pārrakstīt",
"confirmations.redraft.message": "Vai tiešām vēlies dzēst šo ziņu un no jauna noformēt to? Izlase un pastiprinājumi tiks zaudēti, un atbildes uz sākotnējo ziņu tiks atstātas bez autoratlīdzības.",

View File

@@ -3,7 +3,7 @@
"about.contact": "Hubungi:",
"about.disclaimer": "Mastodon ialah perisian sumber terbuka percuma, dan merupakan tanda dagangan Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Sebab tidak tersedia",
"about.domain_blocks.preamble": "Secara amnya, Mastodon membenarkan anda melihat kandungan daripada dan berinteraksi dengan pengguna daripada mana-mana pelayan dalam dunia persekutuan. Berikut ialah pengecualian yang telah dibuat pada pelayan ini secara khususnya.",
"about.domain_blocks.preamble": "Secara amnya, Mastodon membenarkan anda melihat kandungan pengguna daripada mana-mana pelayan dalam alam bersekutu dan berinteraksi dengan mereka. Berikut ialah pengecualian yang khusus pada pelayan ini.",
"about.domain_blocks.silenced.explanation": "Secara amnya, anda tidak akan melihat profil dan kandungan daripada pelayan ini, kecuali anda mencarinya secara khusus atau ikut serta dengan mengikutinya.",
"about.domain_blocks.silenced.title": "Terhad",
"about.domain_blocks.suspended.explanation": "Tiada data daripada pelayan ini yang akan diproses, disimpan atau ditukar, menjadikan sebarang interaksi atau perhubungan dengan pengguna daripada pelayan ini adalah mustahil.",
@@ -19,7 +19,7 @@
"account.block_domain": "Sekat domain {domain}",
"account.block_short": "Malay",
"account.blocked": "Disekat",
"account.cancel_follow_request": "Menarik balik permintaan mengikut",
"account.cancel_follow_request": "Batalkan permintaan ikut",
"account.copy": "Salin pautan ke profil",
"account.direct": "Sebut secara persendirian @{name}",
"account.disable_notifications": "Berhenti maklumkan saya apabila @{name} mengirim hantaran",
@@ -85,10 +85,45 @@
"alert.rate_limited.title": "Kadar terhad",
"alert.unexpected.message": "Berlaku ralat di luar jangkaan.",
"alert.unexpected.title": "Alamak!",
"alt_text_badge.title": "Teks alternatif",
"alt_text_modal.add_alt_text": "Tambah teks alternatif",
"alt_text_modal.add_text_from_image": "Tambah teks dari imej",
"alt_text_modal.cancel": "Batal",
"alt_text_modal.change_thumbnail": "Ubah imej kecil",
"alt_text_modal.describe_for_people_with_hearing_impairments": "Terangkan untuk OKU pendengaran…vorite\n",
"alt_text_modal.describe_for_people_with_visual_impairments": "Terangkan untuk OKU penglihatan…",
"alt_text_modal.done": "Selesai",
"announcement.announcement": "Pengumuman",
"annual_report.summary.archetype.booster": "Si pencapap",
"annual_report.summary.archetype.lurker": "Si penghendap",
"annual_report.summary.archetype.oracle": "Si penilik",
"annual_report.summary.archetype.pollster": "Si peninjau",
"annual_report.summary.archetype.replier": "Si peramah",
"annual_report.summary.followers.followers": "pengikut",
"annual_report.summary.followers.total": "sebanyak {count}",
"annual_report.summary.here_it_is": "Ini ulasan {year} anda:",
"annual_report.summary.highlighted_post.by_favourites": "hantaran paling disukai ramai",
"annual_report.summary.highlighted_post.by_reblogs": "hantaran paling digalak ramai",
"annual_report.summary.highlighted_post.by_replies": "hantaran paling dibalas ramai",
"annual_report.summary.highlighted_post.possessive": "oleh",
"annual_report.summary.most_used_app.most_used_app": "aplikasi paling banyak digunakan",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "tanda pagar paling banyak digunakan",
"annual_report.summary.most_used_hashtag.none": "Tiada",
"annual_report.summary.new_posts.new_posts": "hantaran baharu",
"annual_report.summary.percentile.text": "<topLabel>Anda berkedudukan</topLabel><percentage></percentage><bottomLabel> pengguna {domain}.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Rahsia anda selamat bersama kami. ;)",
"annual_report.summary.thanks": "Terima kasih kerana setia bersama Mastodon!",
"attachments_list.unprocessed": "(belum diproses)",
"audio.hide": "Sembunyikan audio",
"block_modal.remote_users_caveat": "Kami akan meminta pelayan {domain} untuk menghormati keputusan anda. Bagaimanapun, pematuhan tidak dijamin kerana ada pelayan yang mungkin menangani sekatan dengan cara berbeza. Hantaran awam mungkin masih tampak kepada pengguna yang tidak log masuk.",
"block_modal.they_cant_mention": "Dia tidak boleh menyebut tentang anda atau mengikut anda.",
"block_modal.they_cant_see_posts": "Dia tidak boleh melihat hantaran anda dan sebaliknya.",
"block_modal.they_will_know": "Dia boleh lihat bahawa dia disekat.",
"block_modal.title": "Sekat pengguna?",
"block_modal.you_wont_see_mentions": "Anda tidak akan melihat hantaran yang menyebut tentangnya.",
"boost_modal.combo": "Anda boleh tekan {combo} untuk melangkauinya pada waktu lain",
"boost_modal.reblog": "Galakkan hantaran?",
"boost_modal.undo_reblog": "Nyahgalakkan hantaran?",
"bundle_column_error.copy_stacktrace": "Salin laporan ralat",
"bundle_column_error.error.body": "Halaman yang diminta gagal dipaparkan. Ini mungkin disebabkan oleh pepijat dalam kod kami, atau masalah keserasian pelayar.",
"bundle_column_error.error.title": "Alamak!",
@@ -99,6 +134,7 @@
"bundle_column_error.routing.body": "Halaman tersebut tidak dapat ditemui. Adakah anda pasti URL dalam bar alamat adalah betul?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Tutup",
"bundle_modal_error.message": "Terdapat masalah yang dihadapi ketika memuat layar ini.",
"bundle_modal_error.retry": "Cuba lagi",
"closed_registrations.other_server_instructions": "Oleh sebab Mastodon terpencar, anda boleh mencipta akaun pada pelayan lain dan masih berinteraksi dengan pelayan ini.",
"closed_registrations_modal.description": "Mencipta akaun pada {domain} tidak dapat dibuat sekarang, tetapi sila ingat bahawa anda tidak memerlukan akaun khususnya pada {domain} untuk menggunakan Mastodon.",
@@ -109,13 +145,16 @@
"column.blocks": "Pengguna yang disekat",
"column.bookmarks": "Tanda buku",
"column.community": "Garis masa tempatan",
"column.create_list": "Cipta senarai",
"column.direct": "Sebutan peribadi",
"column.directory": "Layari profil",
"column.domain_blocks": "Domain disekat",
"column.favourites": "Kegemaran",
"column.edit_list": "Sunting senarai",
"column.favourites": "Sukaan",
"column.firehose": "Suapan langsung",
"column.follow_requests": "Permintaan ikutan",
"column.home": "Laman Utama",
"column.list_members": "Urus ahli senarai",
"column.lists": "Senarai",
"column.mutes": "Pengguna yang dibisukan",
"column.notifications": "Pemberitahuan",
@@ -128,6 +167,7 @@
"column_header.pin": "Sematkan",
"column_header.show_settings": "Tunjukkan tetapan",
"column_header.unpin": "Nyahsemat",
"column_search.cancel": "Batal",
"column_subheading.settings": "Tetapan",
"community.column_settings.local_only": "Tempatan sahaja",
"community.column_settings.media_only": "Media sahaja",
@@ -146,6 +186,7 @@
"compose_form.poll.duration": "Tempoh undian",
"compose_form.poll.multiple": "Pelbagai pilihan",
"compose_form.poll.option_placeholder": "Pilihan {number}",
"compose_form.poll.single": "Satu pilihan",
"compose_form.poll.switch_to_multiple": "Ubah kepada membenarkan aneka undian",
"compose_form.poll.switch_to_single": "Ubah kepada undian pilihan tunggal",
"compose_form.poll.type": "Gaya",
@@ -160,17 +201,26 @@
"confirmations.block.confirm": "Sekat",
"confirmations.delete.confirm": "Padam",
"confirmations.delete.message": "Adakah anda pasti anda ingin memadam hantaran ini?",
"confirmations.delete.title": "Padam hantaran?",
"confirmations.delete_list.confirm": "Padam",
"confirmations.delete_list.message": "Adakah anda pasti anda ingin memadam senarai ini secara kekal?",
"confirmations.delete_list.title": "Padam senarai?",
"confirmations.discard_edit_media.confirm": "Singkir",
"confirmations.discard_edit_media.message": "Anda belum menyimpan perubahan pada penerangan atau pratonton media. Anda ingin membuangnya?",
"confirmations.edit.confirm": "Sunting",
"confirmations.edit.message": "Mengedit sekarang akan menimpa mesej yang sedang anda karang. Adakah anda pasti mahu meneruskan?",
"confirmations.edit.title": "Tulis ganti hantaran?",
"confirmations.follow_to_list.confirm": "Ikut dan tambah ke senarai",
"confirmations.follow_to_list.message": "Anda mesti mengikuti {name} untuk tambahkannya ke senarai.",
"confirmations.follow_to_list.title": "Ikut pengguna?",
"confirmations.logout.confirm": "Log keluar",
"confirmations.logout.message": "Adakah anda pasti anda ingin log keluar?",
"confirmations.logout.title": "Log keluar?",
"confirmations.missing_alt_text.confirm": "Tambah teks alternatif",
"confirmations.missing_alt_text.message": "Hantaran anda mempunyai media tanpa teks alternatif. Kandungan anda akan lebih mudah tercapai jika anda menambah keterangan.",
"confirmations.mute.confirm": "Bisukan",
"confirmations.redraft.confirm": "Padam & rangka semula",
"confirmations.redraft.message": "Adakah anda pasti anda ingin memadam pos ini dan merangkanya semula? Kegemaran dan galakan akan hilang, dan balasan ke pos asal akan menjadi yatim.",
"confirmations.redraft.message": "Adakah anda pasti anda ingin memadam hantaran ini dan gubal semula? Sukaan dan galakan akan hilang, dan balasan ke hantaran asal akan menjadi yatim.",
"confirmations.reply.confirm": "Balas",
"confirmations.reply.message": "Membalas sekarang akan menulis ganti mesej yang anda sedang karang. Adakah anda pasti anda ingin teruskan?",
"confirmations.unfollow.confirm": "Nyahikut",
@@ -182,7 +232,7 @@
"copy_icon_button.copied": "Disalin ke papan klip",
"copypaste.copied": "Disalin",
"copypaste.copy_to_clipboard": "Salin ke papan klip",
"directory.federated": "Dari fediverse yang diketahui",
"directory.federated": "Dari alam bersekutu yang diketahui",
"directory.local": "Dari {domain} sahaja",
"directory.new_arrivals": "Ketibaan baharu",
"directory.recently_active": "Aktif baru-baru ini",
@@ -190,6 +240,7 @@
"disabled_account_banner.text": "Akaun anda {disabledAccount} telah dinyahaktif.",
"dismissable_banner.community_timeline": "Inilah hantaran awam terkini daripada orang yang akaun dihos oleh {domain}.",
"dismissable_banner.dismiss": "Ketepikan",
"dismissable_banner.explore_statuses": "Hantaran-hantaran dari seluruh alam bersekutu ini sedang sohor. Hantaran terbaharu dengan lebih banyak galakan dan sukaan diberi kedudukan lebih tinggi.",
"embed.instructions": "Benam hantaran ini di laman sesawang anda dengan menyalin kod berikut.",
"embed.preview": "Begini rupanya nanti:",
"emoji_button.activity": "Aktiviti",
@@ -217,8 +268,8 @@
"empty_column.direct": "Anda belum mempunyai sebarang sebutan peribadi lagi. Apabila anda menghantar atau menerima satu, ia akan dipaparkan di sini.",
"empty_column.domain_blocks": "Belum ada domain yang disekat.",
"empty_column.explore_statuses": "Tiada apa-apa yang sohor kini sekarang. Semaklah kemudian!",
"empty_column.favourited_statuses": "Anda belum mempunyai sebarang pos kegemaran. Apabila anda kegemaran, ia akan dipaparkan di sini.",
"empty_column.favourites": "Tiada siapa yang menggemari pos ini lagi. Apabila seseorang melakukannya, mereka akan muncul di sini.",
"empty_column.favourited_statuses": "Anda belum mempunyai sebarang hantaran sukaan lagi. Hantaran akan muncul di sini apabila disukai oleh anda.",
"empty_column.favourites": "Hantaran ini belum disukai mana-mana pengguna lagi. Pengguna yang menyukai akan muncul di sini.",
"empty_column.follow_requests": "Anda belum mempunyai permintaan ikutan. Ia akan terpapar di sini apabila ada nanti.",
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
"empty_column.hashtag": "Belum ada apa-apa dengan tanda pagar ini.",
@@ -300,9 +351,10 @@
"home.pending_critical_update.link": "Lihat pengemaskinian",
"home.pending_critical_update.title": "Kemas kini keselamatan kritikal tersedia!",
"home.show_announcements": "Tunjukkan pengumuman",
"interaction_modal.action.favourite": "Untuk sambung, anda perlu suka dari akaun anda.",
"interaction_modal.on_another_server": "Di pelayan lain",
"interaction_modal.on_this_server": "Pada pelayan ini",
"interaction_modal.title.favourite": "Pos {name} kegemaran",
"interaction_modal.title.favourite": "Suka hantaran {name}",
"interaction_modal.title.follow": "Ikuti {name}",
"interaction_modal.title.reblog": "Galak hantaran {name}",
"interaction_modal.title.reply": "Balas siaran {name}",
@@ -318,8 +370,8 @@
"keyboard_shortcuts.direct": "to open direct messages column",
"keyboard_shortcuts.down": "to move down in the list",
"keyboard_shortcuts.enter": "Buka hantaran",
"keyboard_shortcuts.favourite": "Pos kegemaran",
"keyboard_shortcuts.favourites": "Buka senarai kegemaran",
"keyboard_shortcuts.favourite": "Suka hantaran",
"keyboard_shortcuts.favourites": "Buka senarai sukaan",
"keyboard_shortcuts.federated": "to open federated timeline",
"keyboard_shortcuts.heading": "Pintasan papan kekunci",
"keyboard_shortcuts.home": "to open home timeline",
@@ -367,7 +419,7 @@
"navigation_bar.discover": "Teroka",
"navigation_bar.domain_blocks": "Domain disekat",
"navigation_bar.explore": "Teroka",
"navigation_bar.favourites": "Kegemaran",
"navigation_bar.favourites": "Sukaan",
"navigation_bar.filters": "Perkataan yang dibisukan",
"navigation_bar.follow_requests": "Permintaan ikutan",
"navigation_bar.followed_tags": "Ikuti hashtag",
@@ -385,11 +437,15 @@
"not_signed_in_indicator.not_signed_in": "Anda perlu daftar masuk untuk mencapai sumber ini.",
"notification.admin.report": "{name} melaporkan {target}",
"notification.admin.sign_up": "{name} mendaftar",
"notification.favourite": "{name} menggemari pos anda",
"notification.favourite": "{name} menyukai hantaran anda",
"notification.favourite.name_and_others_with_link": "{name} dan <a>{count, plural, other {# orang lain}}</a> telah suka hantaran anda",
"notification.favourite_pm": "{name} menyukai sebutan persendirian anda",
"notification.favourite_pm.name_and_others_with_link": "{name} dan <a>{count, plural, other {# orang lain}}</a> telah suka sebutan persendirian anda",
"notification.follow": "{name} mengikuti anda",
"notification.follow_request": "{name} meminta untuk mengikuti anda",
"notification.own_poll": "Undian anda telah tamat",
"notification.reblog": "{name} menggalak hantaran anda",
"notification.reblog.name_and_others_with_link": "{name} dan <a>{count, plural, other {# orang lain}}</a> telah galakkan hantaran anda",
"notification.status": "{name} baru sahaja mengirim hantaran",
"notification.update": "{name} menyunting hantaran",
"notifications.clear": "Buang pemberitahuan",
@@ -397,7 +453,7 @@
"notifications.column_settings.admin.report": "Laporan baru:",
"notifications.column_settings.admin.sign_up": "Pendaftaran baru:",
"notifications.column_settings.alert": "Pemberitahuan atas meja",
"notifications.column_settings.favourite": "Kegemaran:",
"notifications.column_settings.favourite": "Sukaan:",
"notifications.column_settings.follow": "Pengikut baharu:",
"notifications.column_settings.follow_request": "Permintaan ikutan baharu:",
"notifications.column_settings.mention": "Sebutan:",
@@ -412,7 +468,7 @@
"notifications.column_settings.update": "Suntingan:",
"notifications.filter.all": "Semua",
"notifications.filter.boosts": "Galakan",
"notifications.filter.favourites": "Kegemaran",
"notifications.filter.favourites": "Sukaan",
"notifications.filter.follows": "Ikutan",
"notifications.filter.mentions": "Sebutan",
"notifications.filter.polls": "Keputusan undian",
@@ -546,7 +602,7 @@
"status.admin_status": "Buka hantaran ini dalam antara muka penyederhanaan",
"status.block": "Sekat @{name}",
"status.bookmark": "Tanda buku",
"status.cancel_reblog_private": "Nyahgalak",
"status.cancel_reblog_private": "Nyahgalakkan",
"status.cannot_reblog": "Hantaran ini tidak boleh digalakkan",
"status.copy": "Salin pautan ke hantaran",
"status.delete": "Padam",
@@ -555,7 +611,8 @@
"status.direct_indicator": "Sebutan peribadi",
"status.edit": "Sunting",
"status.edited_x_times": "Disunting {count, plural, other {{count} kali}}",
"status.favourite": "Kegemaran",
"status.favourite": "Suka",
"status.favourites": "{count, plural, other {sukaan}}",
"status.filter": "Tapiskan hantaran ini",
"status.history.created": "{name} mencipta pada {date}",
"status.history.edited": "{name} menyunting pada {date}",
@@ -572,11 +629,13 @@
"status.pinned": "Hantaran disemat",
"status.read_more": "Baca lagi",
"status.reblog": "Galakkan",
"status.reblog_private": "Galakkan dengan kebolehlihatan asal",
"status.reblogged_by": "{name} telah menggalakkan",
"status.reblogs.empty": "Tiada sesiapa yang menggalak hantaran ini. Apabila ada yang menggalak, ia akan muncul di sini.",
"status.reblog_private": "Galakkan dengan ketampakan asal",
"status.reblogged_by": "{name} galakkan",
"status.reblogs": "{count, plural, other {galakan}}",
"status.reblogs.empty": "Tiada sesiapa yang galakkan hantaran ini. Apabila ada yang galakkan, hantaran akan muncul di sini.",
"status.redraft": "Padam & rangka semula",
"status.remove_bookmark": "Buang tanda buku",
"status.remove_favourite": "Padam dari sukaan",
"status.replied_to": "Menjawab kepada {name}",
"status.reply": "Balas",
"status.replyAll": "Balas ke bebenang",
@@ -612,6 +671,8 @@
"upload_button.label": "Tambah fail imej, video atau audio",
"upload_error.limit": "Sudah melebihi had muat naik.",
"upload_error.poll": "Tidak boleh memuat naik fail bersama undian.",
"upload_form.drag_and_drop.instructions": "Untuk mengangkat lampiran media, tekan jarak atau enter. Ketika menarik, gunakan kekunci anak panah untuk menggerakkan lampiran media pada mana-mana arah. Tekan jarak atau enter untuk melepaskan lampiran media pada kedudukan baharunya, atau tekan keluar untuk batalkan.",
"upload_form.drag_and_drop.on_drag_cancel": "Seretan dibatalkan. Lampiran media {item} dilepaskan.",
"upload_form.edit": "Sunting",
"upload_progress.label": "Memuat naik...",
"upload_progress.processing": "Memproses…",

View File

@@ -697,6 +697,7 @@
"poll_button.remove_poll": "Usuń ankietę",
"privacy.change": "Dostosuj widoczność wpisów",
"privacy.direct.long": "Wszyscy wzmiankowani w tym wpisie",
"privacy.direct.short": "Wzmianka bezpośrednia",
"privacy.private.long": "Tylko obserwujący",
"privacy.private.short": "Obserwujący",
"privacy.public.long": "Każdy na i poza Mastodon",

View File

@@ -697,6 +697,7 @@
"poll_button.remove_poll": "Remover sondagem",
"privacy.change": "Alterar a privacidade da publicação",
"privacy.direct.long": "Todos os mencionados na publicação",
"privacy.direct.short": "Menção privada",
"privacy.private.long": "Apenas os teus seguidores",
"privacy.private.short": "Seguidores",
"privacy.public.long": "Qualquer pessoa no Mastodon ou não",

View File

@@ -697,6 +697,7 @@
"poll_button.remove_poll": "Удалить опрос",
"privacy.change": "Изменить видимость поста",
"privacy.direct.long": "Все упомянутые в посте",
"privacy.direct.short": "Личное упоминание",
"privacy.private.long": "Только ваши подписчики",
"privacy.private.short": "Подписчики",
"privacy.public.long": "Любой, находящийся на Mastodon и вне его",

View File

@@ -700,7 +700,7 @@
"privacy.direct.short": "私下提及",
"privacy.private.long": "仅限你的关注者",
"privacy.private.short": "关注者",
"privacy.public.long": "",
"privacy.public.long": "所有 Mastodon 内外的人",
"privacy.public.short": "公开",
"privacy.unlisted.additional": "此模式的行为与“公开”类似,只是嘟文不会出现在实时动态、话题、探索或 Mastodon 搜索页面中,即使您已全局开启了对应的发现设置。",
"privacy.unlisted.long": "减少算法影响",

View File

@@ -0,0 +1,24 @@
interface BaseMenuItem {
text: string;
dangerous?: boolean;
}
interface ActionMenuItem extends BaseMenuItem {
action: () => void;
}
interface LinkMenuItem extends BaseMenuItem {
to: string;
}
interface ExternalLinkMenuItem extends BaseMenuItem {
href: string;
}
export type MenuItem =
| ActionMenuItem
| LinkMenuItem
| ExternalLinkMenuItem
| null;
export type DropdownMenu = MenuItem[];

View File

@@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { Record as ImmutableRecord } from 'immutable';
import { me } from 'mastodon/initial_state';
import { accountDefaultValues } from 'mastodon/models/account';
import type { Account, AccountShape } from 'mastodon/models/account';
import type { Relationship } from 'mastodon/models/relationship';
@@ -45,3 +46,16 @@ export function makeGetAccount() {
},
);
}
export const getAccountHidden = createSelector(
[
(state: RootState, id: string) => state.accounts.get(id)?.hidden,
(state: RootState, id: string) =>
state.relationships.get(id)?.following ||
state.relationships.get(id)?.requested,
(state: RootState, id: string) => id === me,
],
(hidden, followingOrRequested, isSelf) => {
return hidden && !(isSelf || followingOrRequested);
},
);

View File

@@ -93,27 +93,23 @@ export const makeGetReport = () => createSelector([
export const getAccountGallery = createSelector([
(state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
state => state.get('statuses'),
state => state.get('statuses'),
(state, id) => state.getIn(['accounts', id]),
], (statusIds, statuses, account) => {
let medias = ImmutableList();
statusIds.forEach(statusId => {
const status = statuses.get(statusId).set('account', account);
medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status)));
let status = statuses.get(statusId);
if (status) {
status = status.set('account', account);
medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status)));
}
});
return medias;
});
export const getAccountHidden = createSelector([
(state, id) => state.getIn(['accounts', id, 'hidden']),
(state, id) => state.getIn(['relationships', id, 'following']) || state.getIn(['relationships', id, 'requested']),
(state, id) => id === me,
], (hidden, followingOrRequested, isSelf) => {
return hidden && !(isSelf || followingOrRequested);
});
export const getStatusList = createSelector([
(state, type) => state.getIn(['status_lists', type, 'items']),
], (items) => items.toList());