Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Dalite 2025-04-20 10:16:57 +02:00
commit 37b1d4a2df
293 changed files with 5290 additions and 2874 deletions
.rubocop_todo.yml.ruby-versionCHANGELOG.mdDockerfileFEDERATION.mdGemfile.lockSECURITY.md
app
controllers/api/v1
javascript/mastodon

View file

@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.75.1.
# using RuboCop version 1.75.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@ -58,12 +58,6 @@ Style/FormatStringToken:
Style/GuardClause:
Enabled: false
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/HashTransformValues:
Exclude:
- 'app/serializers/rest/web_push_subscription_serializer.rb'
- 'app/services/import_service.rb'
# Configuration parameters: AllowedMethods.
# AllowedMethods: respond_to_missing?
Style/OptionalBooleanParameter:

View file

@ -1 +1 @@
3.4.2
3.4.3

View file

@ -2,6 +2,29 @@
All notable changes to this project will be documented in this file.
## [4.3.7] - 2025-04-02
### Add
- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire)
- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire)
### Changed
- Change account suspensions to be federated to recently-followed accounts as well (#34294 by @ClearlyClaire)
- Change `AccountReachFinder` to consider statuses based on suspension date (#32805 and #34291 by @ClearlyClaire and @mjankowski)
- Change user archive signed URL TTL from 10 seconds to 1 hour (#34254 by @ClearlyClaire)
### Fixed
- Fix static version of animated PNG emojis not being properly extracted (#34337 by @ClearlyClaire)
- Fix filters not applying in detailed view, favourites and bookmarks (#34259 and #34260 by @ClearlyClaire)
- Fix handling of malformed/unusual HTML (#34201 by @ClearlyClaire)
- Fix `CacheBuster` being queued for missing media attachments (#34253 by @ClearlyClaire)
- Fix incorrect URL being used when cache busting (#34189 by @ClearlyClaire)
- Fix streaming server refusing unix socket path in `DATABASE_URL` (#34091 by @ClearlyClaire)
- Fix “x” hotkey not working on boosted filtered posts (#33758 by @ClearlyClaire)
## [4.3.6] - 2025-03-13
### Security

View file

@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.4.2"
ARG RUBY_VERSION="3.4.3"
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="22"

View file

@ -13,6 +13,7 @@
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
- [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md)
- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md)
- [FEP-044f: Consent-respecting quote posts](https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md): partial support for incoming quote-posts
## ActivityPub in Mastodon

View file

@ -94,7 +94,7 @@ GEM
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.3.2)
aws-partitions (1.1080.0)
aws-partitions (1.1087.0)
aws-sdk-core (3.215.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@ -120,13 +120,13 @@ GEM
rack (>= 0.9.0)
rouge (>= 1.0.0)
bigdecimal (3.1.9)
bindata (2.5.0)
bindata (2.5.1)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.8)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (7.0.1)
brakeman (7.0.2)
racc
browser (6.2.0)
brpoplpush-redis_script (0.1.3)
@ -160,7 +160,7 @@ GEM
cocoon (1.2.15)
color_diff (0.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
connection_pool (2.5.1)
cose (1.3.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@ -170,7 +170,7 @@ GEM
crass (1.0.6)
css_parser (1.21.1)
addressable
csv (3.3.3)
csv (3.3.4)
database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
@ -199,9 +199,9 @@ GEM
activerecord (>= 4.2, < 9.0)
docile (1.4.1)
domain_name (0.6.20240107)
doorkeeper (5.8.1)
doorkeeper (5.8.2)
railties (>= 5)
dotenv (3.1.7)
dotenv (3.1.8)
drb (2.2.1)
elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11)
@ -227,7 +227,7 @@ GEM
fabrication (2.31.0)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
faraday (2.12.2)
faraday (2.13.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
@ -239,7 +239,7 @@ GEM
net-http (>= 0.5.0)
fast_blank (1.0.1)
fastimage (2.4.0)
ffi (1.17.1)
ffi (1.17.2)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
@ -280,7 +280,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.61.1)
haml_lint (0.62.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@ -395,7 +395,7 @@ GEM
rexml
link_header (0.0.8)
lint_roller (1.1.0)
linzer (0.6.3)
linzer (0.6.5)
openssl (~> 3.0, >= 3.0.0)
rack (>= 2.2, < 4.0)
starry (~> 0.2)
@ -426,7 +426,7 @@ GEM
mime-types (3.6.2)
logger
mime-types-data (~> 3.2015)
mime-types-data (3.2025.0402)
mime-types-data (3.2025.0408)
mini_mime (1.1.5)
mini_portile2 (2.8.8)
minitest (5.25.5)
@ -495,6 +495,8 @@ GEM
opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.2)
opentelemetry-semantic_conventions
opentelemetry-helpers-sql (0.1.1)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation (0.3.0)
opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.4.0)
@ -548,8 +550,9 @@ GEM
opentelemetry-instrumentation-net_http (0.23.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-pg (0.30.0)
opentelemetry-instrumentation-pg (0.30.1)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql
opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rack (0.26.0)
@ -585,8 +588,8 @@ GEM
ostruct (0.6.1)
ox (2.14.22)
bigdecimal (>= 3.0)
parallel (1.26.3)
parser (3.3.7.4)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1)
racc
parslet (2.0.0)
@ -697,7 +700,7 @@ GEM
redlock (1.3.2)
redis (>= 3.0.0, < 6.0)
regexp_parser (2.10.0)
reline (0.6.0)
reline (0.6.1)
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)
@ -751,7 +754,7 @@ GEM
rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.44.0)
rubocop-ast (1.44.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
@ -797,7 +800,7 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securerandom (0.4.1)
selenium-webdriver (4.30.1)
selenium-webdriver (4.31.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@ -1085,4 +1088,4 @@ RUBY VERSION
ruby 3.4.1p0
BUNDLED WITH
2.6.7
2.6.8

View file

@ -13,9 +13,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
## Supported Versions
| Version | Supported |
| ------- | ---------------- |
| 4.3.x | Yes |
| 4.2.x | Yes |
| 4.1.x | Until 2025-04-08 |
| < 4.1 | No |
| Version | Supported |
| ------- | --------- |
| 4.3.x | Yes |
| 4.2.x | Yes |
| < 4.2 | No |

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
class Api::V1::Accounts::EndorsementsController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
before_action :require_user!, except: :index
before_action :set_account
before_action :set_endorsed_accounts, only: :index
after_action :insert_pagination_headers, only: :index
def index
cache_if_unauthenticated!
render json: @endorsed_accounts, each_serializer: REST::AccountSerializer
end
def create
AccountPin.find_or_create_by!(account: current_account, target_account: @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
def destroy
pin = AccountPin.find_by(account: current_account, target_account: @account)
pin&.destroy!
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
private
def set_account
@account = Account.find(params[:account_id])
end
def set_endorsed_accounts
@endorsed_accounts = @account.unavailable? ? [] : paginated_endorsed_accounts
end
def paginated_endorsed_accounts
@account.endorsed_accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def relationships_presenter
AccountRelationshipsPresenter.new([@account], current_user.account_id)
end
def next_path
api_v1_account_endorsements_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v1_account_endorsements_url pagination_params(since_id: pagination_since_id) unless @endorsed_accounts.empty?
end
def pagination_collection
@endorsed_accounts
end
def records_continue?
@endorsed_accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
end

View file

@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
end
def set_featured_tags
@featured_tags = @account.suspended? ? [] : @account.featured_tags
@featured_tags = @account.unavailable? ? [] : @account.featured_tags
end
end

View file

@ -1,30 +0,0 @@
# frozen_string_literal: true
class Api::V1::Accounts::PinsController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user!
before_action :set_account
def create
AccountPin.find_or_create_by!(account: current_account, target_account: @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
def destroy
pin = AccountPin.find_by(account: current_account, target_account: @account)
pin&.destroy!
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
private
def set_account
@account = Account.find(params[:account_id])
end
def relationships_presenter
AccountRelationshipsPresenter.new([@account], current_user.account_id)
end
end

View file

@ -7,10 +7,6 @@ class Api::V1::ListsController < Api::BaseController
before_action :require_user!
before_action :set_list, except: [:index, :create]
rescue_from ArgumentError do |e|
render json: { error: e.to_s }, status: 422
end
def index
@lists = List.where(account: current_account).all
render json: @lists, each_serializer: REST::ListSerializer

View file

@ -4,7 +4,7 @@ class Api::V1::SuggestionsController < Api::BaseController
include Authorization
include DeprecationConcern
deprecate_api '2021-05-16'
deprecate_api '2021-05-16', only: [:index]
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index

View file

@ -1,7 +1,9 @@
import { createAction } from '@reduxjs/toolkit';
import { apiRemoveAccountFromFollowers } from 'mastodon/api/accounts';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
export const revealAccount = createAction<{
id: string;
@ -95,3 +97,10 @@ export const fetchRelationshipsSuccess = createAction(
'relationships/fetch/SUCCESS',
actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>,
);
export const removeAccountFromFollowers = createDataLoadingThunk(
'accounts/remove_from_followers',
({ accountId }: { accountId: string }) =>
apiRemoveAccountFromFollowers(accountId),
(relationship) => ({ relationship }),
);

View file

@ -1,11 +1,11 @@
import { createAction } from '@reduxjs/toolkit';
export const openDropdownMenu = createAction<{
id: string;
id: number;
keyboard: boolean;
scrollKey: string;
scrollKey?: string;
}>('dropdownMenu/open');
export const closeDropdownMenu = createAction<{ id: string }>(
export const closeDropdownMenu = createAction<{ id: number }>(
'dropdownMenu/close',
);

View file

@ -1,81 +0,0 @@
import api, { getLinks } from '../api';
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST';
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
export const fetchFollowedHashtags = () => (dispatch) => {
dispatch(fetchFollowedHashtagsRequest());
api().get('/api/v1/followed_tags').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchFollowedHashtagsFail(err));
});
};
export function fetchFollowedHashtagsRequest() {
return {
type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
};
}
export function fetchFollowedHashtagsSuccess(followed_tags, next) {
return {
type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
followed_tags,
next,
};
}
export function fetchFollowedHashtagsFail(error) {
return {
type: FOLLOWED_HASHTAGS_FETCH_FAIL,
error,
};
}
export function expandFollowedHashtags() {
return (dispatch, getState) => {
const url = getState().getIn(['followed_tags', 'next']);
if (url === null) {
return;
}
dispatch(expandFollowedHashtagsRequest());
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFollowedHashtagsFail(error));
});
};
}
export function expandFollowedHashtagsRequest() {
return {
type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
};
}
export function expandFollowedHashtagsSuccess(followed_tags, next) {
return {
type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
followed_tags,
next,
};
}
export function expandFollowedHashtagsFail(error) {
return {
type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
error,
};
}

View file

@ -18,3 +18,8 @@ export const apiFollowAccount = (
export const apiUnfollowAccount = (id: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/unfollow`);
export const apiRemoveAccountFromFollowers = (id: string) =>
apiRequestPost<ApiRelationshipJSON>(
`v1/accounts/${id}/remove_from_followers`,
);

View file

@ -1,4 +1,4 @@
import { apiRequestPost, apiRequestGet } from 'mastodon/api';
import api, { getLinks, apiRequestPost, apiRequestGet } from 'mastodon/api';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
export const apiGetTag = (tagId: string) =>
@ -9,3 +9,15 @@ export const apiFollowTag = (tagId: string) =>
export const apiUnfollowTag = (tagId: string) =>
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfollow`);
export const apiGetFollowedTags = async (url?: string) => {
const response = await api().request<ApiHashtagJSON[]>({
method: 'GET',
url: url ?? '/api/v1/followed_tags',
});
return {
tags: response.data,
links: getLinks(response),
};
};

View file

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@ -12,18 +12,19 @@ import {
muteAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import { FollowersCounter } from 'mastodon/components/counters';
import { DisplayName } from 'mastodon/components/display_name';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { FollowButton } from 'mastodon/components/follow_button';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import DropdownMenu from 'mastodon/containers/dropdown_menu_container';
import { me } from 'mastodon/initial_state';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
@ -46,6 +47,14 @@ const messages = defineMessages({
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
more: { id: 'status.more', defaultMessage: 'More' },
addToLists: {
id: 'account.add_or_remove_from_list',
defaultMessage: 'Add or Remove from lists',
},
openOriginalPage: {
id: 'account.open_original_page',
defaultMessage: 'Open original page',
},
});
export const Account: React.FC<{
@ -60,6 +69,7 @@ export const Account: React.FC<{
const account = useAppSelector((state) => state.accounts.get(id));
const relationship = useAppSelector((state) => state.relationships.get(id));
const dispatch = useAppDispatch();
const accountUrl = account?.url;
const handleBlock = useCallback(() => {
if (relationship?.blocking) {
@ -77,13 +87,62 @@ export const Account: React.FC<{
}
}, [dispatch, id, account, relationship]);
const handleMuteNotifications = useCallback(() => {
dispatch(muteAccount(id, true));
}, [dispatch, id]);
const menu = useMemo(() => {
let arr: MenuItem[] = [];
const handleUnmuteNotifications = useCallback(() => {
dispatch(muteAccount(id, false));
}, [dispatch, id]);
if (defaultAction === 'mute') {
const handleMuteNotifications = () => {
dispatch(muteAccount(id, true));
};
const handleUnmuteNotifications = () => {
dispatch(muteAccount(id, false));
};
arr = [
{
text: intl.formatMessage(
relationship?.muting_notifications
? messages.unmute_notifications
: messages.mute_notifications,
),
action: relationship?.muting_notifications
? handleUnmuteNotifications
: handleMuteNotifications,
},
];
} else if (defaultAction !== 'block') {
const handleAddToLists = () => {
dispatch(
openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: id,
},
}),
);
};
arr = [
{
text: intl.formatMessage(messages.addToLists),
action: handleAddToLists,
},
];
if (accountUrl) {
arr.unshift(
{
text: intl.formatMessage(messages.openOriginalPage),
href: accountUrl,
},
null,
);
}
}
return arr;
}, [dispatch, intl, id, accountUrl, relationship, defaultAction]);
if (hidden) {
return (
@ -94,69 +153,42 @@ export const Account: React.FC<{
);
}
let buttons;
let button: React.ReactNode, dropdown: React.ReactNode;
if (account && account.id !== me && relationship) {
const { requested, blocking, muting } = relationship;
if (requested) {
buttons = <FollowButton accountId={id} />;
} else if (blocking) {
buttons = (
<Button
text={intl.formatMessage(messages.unblock)}
onClick={handleBlock}
/>
);
} else if (muting) {
const menu = [
{
text: intl.formatMessage(
relationship.muting_notifications
? messages.unmute_notifications
: messages.mute_notifications,
),
action: relationship.muting_notifications
? handleUnmuteNotifications
: handleMuteNotifications,
},
];
buttons = (
<>
<DropdownMenu
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
<Button
text={intl.formatMessage(messages.unmute)}
onClick={handleMute}
/>
</>
);
} else if (defaultAction === 'mute') {
buttons = (
<Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />
);
} else if (defaultAction === 'block') {
buttons = (
<Button
text={intl.formatMessage(messages.block)}
onClick={handleBlock}
/>
);
} else {
buttons = <FollowButton accountId={id} />;
}
} else {
buttons = <FollowButton accountId={id} />;
if (menu.length > 0) {
dropdown = (
<Dropdown
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
title={intl.formatMessage(messages.more)}
/>
);
}
let muteTimeRemaining;
if (defaultAction === 'block') {
button = (
<Button
text={intl.formatMessage(
relationship?.blocking ? messages.unblock : messages.block,
)}
onClick={handleBlock}
/>
);
} else if (defaultAction === 'mute') {
button = (
<Button
text={intl.formatMessage(
relationship?.muting ? messages.unmute : messages.mute,
)}
onClick={handleMute}
/>
);
} else {
button = <FollowButton accountId={id} />;
}
let muteTimeRemaining: React.ReactNode;
if (account?.mute_expires_at) {
muteTimeRemaining = (
@ -166,7 +198,7 @@ export const Account: React.FC<{
);
}
let verification;
let verification: React.ReactNode;
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
@ -212,7 +244,12 @@ export const Account: React.FC<{
</div>
</Link>
{!minimal && <div className='account__relationship'>{buttons}</div>}
{!minimal && (
<div className='account__relationship'>
{dropdown}
{button}
</div>
)}
</div>
{account &&

View file

@ -1,343 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent, cloneElement, Children } from 'react';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';
import { CircularProgress } from 'mastodon/components/circular_progress';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { IconButton } from './icon_button';
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
let id = 0;
class DropdownMenu extends PureComponent {
static propTypes = {
items: PropTypes.array.isRequired,
loading: PropTypes.bool,
scrollable: PropTypes.bool,
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
openedViaKeyboard: PropTypes.bool,
renderItem: PropTypes.func,
renderHeader: PropTypes.func,
onItemClick: PropTypes.func.isRequired,
};
static defaultProps = {
style: {},
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
e.stopPropagation();
e.preventDefault();
}
};
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, { capture: true });
document.addEventListener('keydown', this.handleKeyDown, { capture: true });
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus({ preventScroll: true });
}
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
document.removeEventListener('keydown', this.handleKeyDown, { capture: true });
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
};
setFocusRef = c => {
this.focusedItem = c;
};
handleKeyDown = e => {
const items = Array.from(this.node.querySelectorAll('a, button'));
const index = items.indexOf(document.activeElement);
let element = null;
switch(e.key) {
case 'ArrowDown':
element = items[index+1] || items[0];
break;
case 'ArrowUp':
element = items[index-1] || items[items.length-1];
break;
case 'Tab':
if (e.shiftKey) {
element = items[index-1] || items[items.length-1];
} else {
element = items[index+1] || items[0];
}
break;
case 'Home':
element = items[0];
break;
case 'End':
element = items[items.length-1];
break;
case 'Escape':
this.props.onClose();
break;
}
if (element) {
element.focus();
e.preventDefault();
e.stopPropagation();
}
};
handleItemKeyPress = e => {
if (e.key === 'Enter' || e.key === ' ') {
this.handleClick(e);
}
};
handleClick = e => {
const { onItemClick } = this.props;
onItemClick(e);
};
renderItem = (option, i) => {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, href = '#', target = '_blank', method, dangerous } = option;
return (
<li className={classNames('dropdown-menu__item', { 'dropdown-menu__item--dangerous': dangerous })} key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text}
</a>
</li>
);
};
render () {
const { items, scrollable, renderHeader, loading } = this.props;
let renderItem = this.props.renderItem || this.renderItem;
return (
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })} ref={this.setRef}>
{loading && (
<CircularProgress size={30} strokeWidth={3.5} />
)}
{!loading && renderHeader && (
<div className='dropdown-menu__container__header'>
{renderHeader(items)}
</div>
)}
{!loading && (
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
</ul>
)}
</div>
);
}
}
class Dropdown extends PureComponent {
static propTypes = {
children: PropTypes.node,
icon: PropTypes.string,
iconComponent: PropTypes.func,
items: PropTypes.array.isRequired,
loading: PropTypes.bool,
size: PropTypes.number,
title: PropTypes.string,
disabled: PropTypes.bool,
scrollable: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
renderItem: PropTypes.func,
renderHeader: PropTypes.func,
onItemClick: PropTypes.func,
...WithRouterPropTypes
};
static defaultProps = {
title: 'Menu',
};
state = {
id: id++,
};
handleClick = ({ type }) => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
} else {
this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click');
}
};
handleClose = () => {
if (this.activeElement) {
this.activeElement.focus({ preventScroll: true });
this.activeElement = null;
}
this.props.onClose(this.state.id);
};
handleMouseDown = () => {
if (!this.state.open) {
this.activeElement = document.activeElement;
}
};
handleButtonKeyDown = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleMouseDown();
break;
}
};
handleKeyPress = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.stopPropagation();
e.preventDefault();
break;
}
};
handleItemClick = e => {
const { onItemClick } = this.props;
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = this.props.items[i];
this.handleClose();
if (typeof onItemClick === 'function') {
e.preventDefault();
onItemClick(item, i);
} else if (item && typeof item.action === 'function') {
e.preventDefault();
item.action();
} else if (item && item.to) {
e.preventDefault();
this.props.history.push(item.to);
}
};
setTargetRef = c => {
this.target = c;
};
findTarget = () => {
return this.target?.buttonRef?.current ?? this.target;
};
componentWillUnmount = () => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
}
};
close = () => {
this.handleClose();
};
render () {
const {
icon,
iconComponent,
items,
size,
title,
disabled,
loading,
scrollable,
openDropdownId,
openedViaKeyboard,
children,
renderItem,
renderHeader,
} = this.props;
const open = this.state.id === openDropdownId;
const button = children ? cloneElement(Children.only(children), {
onClick: this.handleClick,
onMouseDown: this.handleMouseDown,
onKeyDown: this.handleButtonKeyDown,
onKeyPress: this.handleKeyPress,
ref: this.setTargetRef,
}) : (
<IconButton
icon={!open ? icon : 'close'}
iconComponent={iconComponent}
title={title}
active={open}
disabled={disabled}
size={size}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
ref={this.setTargetRef}
/>
);
return (
<>
{button}
<Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
{({ props, arrowProps, placement }) => (
<div {...props}>
<div className={`dropdown-animation dropdown-menu ${placement}`}>
<div className={`dropdown-menu__arrow ${placement}`} {...arrowProps} />
<DropdownMenu
items={items}
loading={loading}
scrollable={scrollable}
onClose={this.handleClose}
openedViaKeyboard={openedViaKeyboard}
renderItem={renderItem}
renderHeader={renderHeader}
onItemClick={this.handleItemClick}
/>
</div>
</div>
)}
</Overlay>
</>
);
}
}
export default withRouter(Dropdown);

View file

@ -0,0 +1,549 @@
import {
useState,
useEffect,
useRef,
useCallback,
cloneElement,
Children,
} from 'react';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { Map as ImmutableMap } from 'immutable';
import Overlay from 'react-overlays/Overlay';
import type {
OffsetValue,
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
import { fetchRelationships } from 'mastodon/actions/accounts';
import {
openDropdownMenu,
closeDropdownMenu,
} from 'mastodon/actions/dropdown_menu';
import { openModal, closeModal } from 'mastodon/actions/modal';
import { CircularProgress } from 'mastodon/components/circular_progress';
import { isUserTouching } from 'mastodon/is_mobile';
import type {
MenuItem,
ActionMenuItem,
ExternalLinkMenuItem,
} from 'mastodon/models/dropdown_menu';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import type { IconProp } from './icon';
import { IconButton } from './icon_button';
let id = 0;
const isMenuItem = (item: unknown): item is MenuItem => {
if (item === null) {
return true;
}
return typeof item === 'object' && 'text' in item;
};
const isActionItem = (item: unknown): item is ActionMenuItem => {
if (!item || !isMenuItem(item)) {
return false;
}
return 'action' in item;
};
const isExternalLinkItem = (item: unknown): item is ExternalLinkMenuItem => {
if (!item || !isMenuItem(item)) {
return false;
}
return 'href' in item;
};
type RenderItemFn<Item = MenuItem> = (
item: Item,
index: number,
handlers: {
onClick: (e: React.MouseEvent) => void;
onKeyUp: (e: React.KeyboardEvent) => void;
},
) => React.ReactNode;
type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
type RenderHeaderFn<Item = MenuItem> = (items: Item[]) => React.ReactNode;
interface DropdownMenuProps<Item = MenuItem> {
items?: Item[];
loading?: boolean;
scrollable?: boolean;
onClose: () => void;
openedViaKeyboard: boolean;
renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>;
onItemClick?: ItemClickFn<Item>;
}
export const DropdownMenu = <Item = MenuItem,>({
items,
loading,
scrollable,
onClose,
openedViaKeyboard,
renderItem,
renderHeader,
onItemClick,
}: DropdownMenuProps<Item>) => {
const nodeRef = useRef<HTMLDivElement>(null);
const focusedItemRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const handleDocumentClick = (e: MouseEvent) => {
if (
e.target instanceof Node &&
nodeRef.current &&
!nodeRef.current.contains(e.target)
) {
onClose();
e.stopPropagation();
e.preventDefault();
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (!nodeRef.current) {
return;
}
const items = Array.from(nodeRef.current.querySelectorAll('a, button'));
const index = document.activeElement
? items.indexOf(document.activeElement)
: -1;
let element: Element | undefined;
switch (e.key) {
case 'ArrowDown':
element = items[index + 1] ?? items[0];
break;
case 'ArrowUp':
element = items[index - 1] ?? items[items.length - 1];
break;
case 'Tab':
if (e.shiftKey) {
element = items[index - 1] ?? items[items.length - 1];
} else {
element = items[index + 1] ?? items[0];
}
break;
case 'Home':
element = items[0];
break;
case 'End':
element = items[items.length - 1];
break;
case 'Escape':
onClose();
break;
}
if (element && element instanceof HTMLElement) {
element.focus();
e.preventDefault();
e.stopPropagation();
}
};
document.addEventListener('click', handleDocumentClick, { capture: true });
document.addEventListener('keydown', handleKeyDown, { capture: true });
if (focusedItemRef.current && openedViaKeyboard) {
focusedItemRef.current.focus({ preventScroll: true });
}
return () => {
document.removeEventListener('click', handleDocumentClick, {
capture: true,
});
document.removeEventListener('keydown', handleKeyDown, { capture: true });
};
}, [onClose, openedViaKeyboard]);
const handleFocusedItemRef = useCallback(
(c: HTMLAnchorElement | HTMLButtonElement | null) => {
focusedItemRef.current = c as HTMLElement;
},
[],
);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = items?.[i];
onClose();
if (!item) {
return;
}
if (typeof onItemClick === 'function') {
e.preventDefault();
onItemClick(item, i);
} else if (isActionItem(item)) {
e.preventDefault();
item.action();
}
},
[onClose, onItemClick, items],
);
const handleItemKeyUp = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
handleItemClick(e);
}
},
[handleItemClick],
);
const nativeRenderItem = (option: Item, i: number) => {
if (!isMenuItem(option)) {
return null;
}
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, dangerous } = option;
let element: React.ReactElement;
if (isActionItem(option)) {
element = (
<button
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
{text}
</button>
);
} else if (isExternalLinkItem(option)) {
element = (
<a
href={option.href}
target={option.target ?? '_target'}
data-method={option.method}
rel='noopener'
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
{text}
</a>
);
} else {
element = (
<Link
to={option.to}
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
{text}
</Link>
);
}
return (
<li
className={classNames('dropdown-menu__item', {
'dropdown-menu__item--dangerous': dangerous,
})}
key={`${text}-${i}`}
>
{element}
</li>
);
};
const renderItemMethod = renderItem ?? nativeRenderItem;
return (
<div
className={classNames('dropdown-menu__container', {
'dropdown-menu__container--loading': loading,
})}
ref={nodeRef}
>
{(loading || !items) && <CircularProgress size={30} strokeWidth={3.5} />}
{!loading && renderHeader && items && (
<div className='dropdown-menu__container__header'>
{renderHeader(items)}
</div>
)}
{!loading && items && (
<ul
className={classNames('dropdown-menu__container__list', {
'dropdown-menu__container__list--scrollable': scrollable,
})}
>
{items.map((option, i) =>
renderItemMethod(option, i, {
onClick: handleItemClick,
onKeyUp: handleItemKeyUp,
}),
)}
</ul>
)}
</div>
);
};
interface DropdownProps<Item = MenuItem> {
children?: React.ReactElement;
icon?: string;
iconComponent?: IconProp;
items?: Item[];
loading?: boolean;
title?: string;
disabled?: boolean;
scrollable?: boolean;
scrollKey?: string;
status?: ImmutableMap<string, unknown>;
renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>;
onOpen?: () => void;
onItemClick?: ItemClickFn<Item>;
}
const offset = [5, 5] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
export const Dropdown = <Item = MenuItem,>({
children,
icon,
iconComponent,
items,
loading,
title = 'Menu',
disabled,
scrollable,
status,
renderItem,
renderHeader,
onOpen,
onItemClick,
scrollKey,
}: DropdownProps<Item>) => {
const dispatch = useAppDispatch();
const openDropdownId = useAppSelector((state) => state.dropdownMenu.openId);
const openedViaKeyboard = useAppSelector(
(state) => state.dropdownMenu.keyboard,
);
const [currentId] = useState(id++);
const open = currentId === openDropdownId;
const activeElement = useRef<HTMLElement | null>(null);
const targetRef = useRef<HTMLButtonElement | null>(null);
const handleClose = useCallback(() => {
if (activeElement.current) {
activeElement.current.focus({ preventScroll: true });
activeElement.current = null;
}
dispatch(
closeModal({
modalType: 'ACTIONS',
ignoreFocus: false,
}),
);
dispatch(closeDropdownMenu({ id: currentId }));
}, [dispatch, currentId]);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = items?.[i];
handleClose();
if (!item) {
return;
}
if (typeof onItemClick === 'function') {
e.preventDefault();
onItemClick(item, i);
} else if (isActionItem(item)) {
e.preventDefault();
item.action();
}
},
[handleClose, onItemClick, items],
);
const handleClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const { type } = e;
if (open) {
handleClose();
} else {
onOpen?.();
if (status) {
dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
}
if (isUserTouching()) {
dispatch(
openModal({
modalType: 'ACTIONS',
modalProps: {
status,
actions: items,
onClick: handleItemClick,
},
}),
);
} else {
dispatch(
openDropdownMenu({
id: currentId,
keyboard: type !== 'click',
scrollKey,
}),
);
}
}
},
[
dispatch,
currentId,
scrollKey,
onOpen,
handleItemClick,
open,
status,
items,
handleClose,
],
);
const handleMouseDown = useCallback(() => {
if (!open && document.activeElement instanceof HTMLElement) {
activeElement.current = document.activeElement;
}
}, [open]);
const handleButtonKeyDown = useCallback(
(e: React.KeyboardEvent) => {
switch (e.key) {
case ' ':
case 'Enter':
handleMouseDown();
break;
}
},
[handleMouseDown],
);
const handleKeyPress = useCallback(
(e: React.KeyboardEvent) => {
switch (e.key) {
case ' ':
case 'Enter':
handleClick(e);
e.stopPropagation();
e.preventDefault();
break;
}
},
[handleClick],
);
useEffect(() => {
return () => {
if (currentId === openDropdownId) {
handleClose();
}
};
}, [currentId, openDropdownId, handleClose]);
let button: React.ReactElement;
if (children) {
button = cloneElement(Children.only(children), {
onClick: handleClick,
onMouseDown: handleMouseDown,
onKeyDown: handleButtonKeyDown,
onKeyPress: handleKeyPress,
ref: targetRef,
});
} else if (icon && iconComponent) {
button = (
<IconButton
icon={!open ? icon : 'close'}
iconComponent={iconComponent}
title={title}
active={open}
disabled={disabled}
onClick={handleClick}
onMouseDown={handleMouseDown}
onKeyDown={handleButtonKeyDown}
onKeyPress={handleKeyPress}
ref={targetRef}
/>
);
} else {
return null;
}
return (
<>
{button}
<Overlay
show={open}
offset={offset}
placement='bottom'
flip
target={targetRef}
popperConfig={popperConfig}
>
{({ props, arrowProps, placement }) => (
<div {...props}>
<div className={`dropdown-animation dropdown-menu ${placement}`}>
<div
className={`dropdown-menu__arrow ${placement}`}
{...arrowProps}
/>
<DropdownMenu
items={items}
loading={loading}
scrollable={scrollable}
onClose={handleClose}
openedViaKeyboard={openedViaKeyboard}
renderItem={renderItem}
renderHeader={renderHeader}
onItemClick={onItemClick}
/>
</div>
</div>
)}
</Overlay>
</>
);
};

View file

@ -1,32 +0,0 @@
import { connect } from 'react-redux';
import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_menu';
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.dropdownMenu.openId,
openedViaKeyboard: state.dropdownMenu.keyboard,
items: state.getIn(['history', statusId, 'items']),
loading: state.getIn(['history', statusId, 'loading']),
});
const mapDispatchToProps = (dispatch, { statusId }) => ({
onOpen (id, onItemClick, keyboard) {
dispatch(fetchHistory(statusId));
dispatch(openDropdownMenu({ id, keyboard }));
},
onClose (id) {
dispatch(closeDropdownMenu({ id }));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);

View file

@ -1,77 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
import InlineAccount from 'mastodon/components/inline_account';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import DropdownMenu from './containers/dropdown_menu_container';
const mapDispatchToProps = (dispatch, { statusId }) => ({
onItemClick (index) {
dispatch(openModal({
modalType: 'COMPARE_HISTORY',
modalProps: { index, statusId },
}));
},
});
class EditedTimestamp extends PureComponent {
static propTypes = {
statusId: PropTypes.string.isRequired,
timestamp: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired,
onItemClick: PropTypes.func.isRequired,
};
handleItemClick = (item, i) => {
const { onItemClick } = this.props;
onItemClick(i);
};
renderHeader = items => {
return (
<FormattedMessage id='status.edited_x_times' defaultMessage='Edited {count, plural, one {# time} other {# times}}' values={{ count: items.size - 1 }} />
);
};
renderItem = (item, index, { onClick, onKeyPress }) => {
const formattedDate = <RelativeTimestamp timestamp={item.get('created_at')} short={false} />;
const formattedName = <InlineAccount accountId={item.get('account')} />;
const label = item.get('original') ? (
<FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
) : (
<FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
);
return (
<li className='dropdown-menu__item edited-timestamp__history__item' key={item.get('created_at')}>
<button data-index={index} onClick={onClick} onKeyPress={onKeyPress}>{label}</button>
</li>
);
};
render () {
const { timestamp, statusId } = this.props;
return (
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
<button className='dropdown-menu__text-button'>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <FormattedDateWrapper className='animated-number' value={timestamp} month='short' day='2-digit' hour='2-digit' minute='2-digit' /> }} />
</button>
</DropdownMenu>
);
}
}
export default connect(null, mapDispatchToProps)(injectIntl(EditedTimestamp));

View file

@ -0,0 +1,140 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { fetchHistory } from 'mastodon/actions/history';
import { openModal } from 'mastodon/actions/modal';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
import InlineAccount from 'mastodon/components/inline_account';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
type HistoryItem = ImmutableMap<string, unknown>;
export const EditedTimestamp: React.FC<{
statusId: string;
timestamp: string;
}> = ({ statusId, timestamp }) => {
const dispatch = useAppDispatch();
const items = useAppSelector(
(state) =>
(
state.history.getIn([statusId, 'items']) as
| ImmutableList<unknown>
| undefined
)?.toArray() as HistoryItem[],
);
const loading = useAppSelector(
(state) => state.history.getIn([statusId, 'loading']) as boolean,
);
const handleOpen = useCallback(() => {
dispatch(fetchHistory(statusId));
}, [dispatch, statusId]);
const handleItemClick = useCallback(
(_item: HistoryItem, index: number) => {
dispatch(
openModal({
modalType: 'COMPARE_HISTORY',
modalProps: { index, statusId },
}),
);
},
[dispatch, statusId],
);
const renderHeader = useCallback((items: HistoryItem[]) => {
return (
<FormattedMessage
id='status.edited_x_times'
defaultMessage='Edited {count, plural, one {# time} other {# times}}'
values={{ count: items.length - 1 }}
/>
);
}, []);
const renderItem = useCallback(
(
item: HistoryItem,
index: number,
{
onClick,
onKeyUp,
}: {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
},
) => {
const formattedDate = (
<RelativeTimestamp
timestamp={item.get('created_at') as string}
short={false}
/>
);
const formattedName = (
<InlineAccount accountId={item.get('account') as string} />
);
const label = (item.get('original') as boolean) ? (
<FormattedMessage
id='status.history.created'
defaultMessage='{name} created {date}'
values={{ name: formattedName, date: formattedDate }}
/>
) : (
<FormattedMessage
id='status.history.edited'
defaultMessage='{name} edited {date}'
values={{ name: formattedName, date: formattedDate }}
/>
);
return (
<li
className='dropdown-menu__item edited-timestamp__history__item'
key={item.get('created_at') as string}
>
<button data-index={index} onClick={onClick} onKeyUp={onKeyUp}>
{label}
</button>
</li>
);
},
[],
);
return (
<Dropdown<HistoryItem>
items={items}
loading={loading}
renderItem={renderItem}
scrollable
renderHeader={renderHeader}
onOpen={handleOpen}
onItemClick={handleItemClick}
>
<button className='dropdown-menu__text-button'>
<FormattedMessage
id='status.edited'
defaultMessage='Edited {date}'
values={{
date: (
<FormattedDateWrapper
className='animated-number'
value={timestamp}
month='short'
day='2-digit'
hour='2-digit'
minute='2-digit'
/>
),
}}
/>
</button>
</Dropdown>
);
};

View file

@ -16,8 +16,7 @@ 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' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
editProfile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
});
export const FollowButton: React.FC<{
@ -73,11 +72,9 @@ export const FollowButton: React.FC<{
if (!signedIn) {
label = intl.formatMessage(messages.follow);
} else if (accountId === me) {
label = intl.formatMessage(messages.edit_profile);
label = intl.formatMessage(messages.editProfile);
} else if (!relationship) {
label = <LoadingIndicator />;
} else if (relationship.following && relationship.followed_by) {
label = intl.formatMessage(messages.mutual);
} else if (relationship.following || relationship.requested) {
label = intl.formatMessage(messages.unfollow);
} else if (relationship.followed_by) {

View file

@ -102,10 +102,11 @@ export interface HashtagProps {
description?: React.ReactNode;
history?: number[];
name: string;
people: number;
people?: number;
to: string;
uses?: number;
withGraph?: boolean;
children?: React.ReactNode;
}
export const Hashtag: React.FC<HashtagProps> = ({
@ -117,6 +118,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
className,
description,
withGraph = true,
children,
}) => (
<div className={classNames('trends__item', className)}>
<div className='trends__item__name'>
@ -158,5 +160,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
</SilentErrorBoundary>
</div>
)}
{children && <div className='trends__item__buttons'>{children}</div>}
</div>
);

View file

@ -20,6 +20,7 @@ export type StatusLike = Record<{
contentHTML: string;
media_attachments: List<unknown>;
spoiler_text?: string;
account: Record<{ id: string }>;
}>;
function normalizeHashtag(hashtag: string) {
@ -195,13 +196,19 @@ export function getHashtagBarForStatus(status: StatusLike) {
return {
statusContentProps,
hashtagBar: <HashtagBar hashtags={hashtagsInBar} />,
hashtagBar: (
<HashtagBar
hashtags={hashtagsInBar}
accountId={status.getIn(['account', 'id']) as string}
/>
),
};
}
const HashtagBar: React.FC<{
hashtags: string[];
}> = ({ hashtags }) => {
accountId: string;
}> = ({ hashtags, accountId }) => {
const [expanded, setExpanded] = useState(false);
const handleClick = useCallback(() => {
setExpanded(true);
@ -218,7 +225,11 @@ const HashtagBar: React.FC<{
return (
<div className='hashtag-bar'>
{revealedHashtags.map((hashtag) => (
<Link key={hashtag} to={`/tags/${hashtag}`}>
<Link
key={hashtag}
to={`/tags/${hashtag}`}
data-menu-hashtag={accountId}
>
#<span>{hashtag}</span>
</Link>
))}

View file

@ -1,4 +1,4 @@
import { PureComponent, createRef } from 'react';
import { useState, useEffect, useCallback, forwardRef } from 'react';
import classNames from 'classnames';
@ -15,99 +15,108 @@ interface Props {
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
active: boolean;
active?: boolean;
expanded?: boolean;
style?: React.CSSProperties;
activeStyle?: React.CSSProperties;
disabled: boolean;
disabled?: boolean;
inverted?: boolean;
animate: boolean;
overlay: boolean;
tabIndex: number;
animate?: boolean;
overlay?: boolean;
tabIndex?: number;
counter?: number;
href?: string;
ariaHidden: boolean;
ariaHidden?: boolean;
}
interface States {
activate: boolean;
deactivate: boolean;
}
export class IconButton extends PureComponent<Props, States> {
buttonRef = createRef<HTMLButtonElement>();
static defaultProps = {
active: false,
disabled: false,
animate: false,
overlay: false,
tabIndex: 0,
ariaHidden: false,
};
state = {
activate: false,
deactivate: false,
};
UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (!nextProps.animate) return;
if (this.props.active && !nextProps.active) {
this.setState({ activate: false, deactivate: true });
} else if (!this.props.active && nextProps.active) {
this.setState({ activate: true, deactivate: false });
}
}
handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
if (!this.props.disabled && this.props.onClick != null) {
this.props.onClick(e);
}
};
handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
if (this.props.onKeyPress && !this.props.disabled) {
this.props.onKeyPress(e);
}
};
handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
if (!this.props.disabled && this.props.onMouseDown) {
this.props.onMouseDown(e);
}
};
handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
if (!this.props.disabled && this.props.onKeyDown) {
this.props.onKeyDown(e);
}
};
render() {
const style = {
...this.props.style,
...(this.props.active ? this.props.activeStyle : {}),
};
const {
active,
export const IconButton = forwardRef<HTMLButtonElement, Props>(
(
{
className,
disabled,
expanded,
icon,
iconComponent,
inverted,
overlay,
tabIndex,
title,
counter,
href,
ariaHidden,
} = this.props;
style,
activeStyle,
onClick,
onKeyDown,
onKeyPress,
onMouseDown,
active = false,
disabled = false,
animate = false,
overlay = false,
tabIndex = 0,
ariaHidden = false,
},
buttonRef,
) => {
const [activate, setActivate] = useState(false);
const [deactivate, setDeactivate] = useState(false);
const { activate, deactivate } = this.state;
useEffect(() => {
if (!animate) {
return;
}
if (activate && !active) {
setActivate(false);
setDeactivate(true);
} else if (!activate && active) {
setActivate(true);
setDeactivate(false);
}
}, [setActivate, setDeactivate, animate, active, activate]);
const handleClick: React.MouseEventHandler<HTMLButtonElement> = useCallback(
(e) => {
e.preventDefault();
if (!disabled) {
onClick?.(e);
}
},
[disabled, onClick],
);
const handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> =
useCallback(
(e) => {
if (!disabled) {
onKeyPress?.(e);
}
},
[disabled, onKeyPress],
);
const handleMouseDown: React.MouseEventHandler<HTMLButtonElement> =
useCallback(
(e) => {
if (!disabled) {
onMouseDown?.(e);
}
},
[disabled, onMouseDown],
);
const handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> =
useCallback(
(e) => {
if (!disabled) {
onKeyDown?.(e);
}
},
[disabled, onKeyDown],
);
const buttonStyle = {
...style,
...(active ? activeStyle : {}),
};
const classes = classNames(className, 'icon-button', {
active,
@ -146,18 +155,19 @@ export class IconButton extends PureComponent<Props, States> {
aria-hidden={ariaHidden}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
// eslint-disable-next-line @typescript-eslint/no-deprecated
onKeyPress={this.handleKeyPress}
style={style}
onClick={handleClick}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
onKeyPress={handleKeyPress} // eslint-disable-line @typescript-eslint/no-deprecated
style={buttonStyle}
tabIndex={tabIndex}
disabled={disabled}
ref={this.buttonRef}
ref={buttonRef}
>
{contents}
</button>
);
}
}
},
);
IconButton.displayName = 'IconButton';

View file

@ -1,25 +1,6 @@
import { Switch, Route } from 'react-router-dom';
import AccountNavigation from 'mastodon/features/account/navigation';
import Trends from 'mastodon/features/getting_started/containers/trends_container';
import { showTrends } from 'mastodon/initial_state';
const DefaultNavigation: React.FC = () => (showTrends ? <Trends /> : null);
export const NavigationPortal: React.FC = () => (
<div className='navigation-panel__portal'>
<Switch>
<Route path='/@:acct' exact component={AccountNavigation} />
<Route
path='/@:acct/tagged/:tagged?'
exact
component={AccountNavigation}
/>
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
<Route path='/@:acct/followers' exact component={AccountNavigation} />
<Route path='/@:acct/following' exact component={AccountNavigation} />
<Route path='/@:acct/media' exact component={AccountNavigation} />
<Route component={DefaultNavigation} />
</Switch>
</div>
<div className='navigation-panel__portal'>{showTrends && <Trends />}</div>
);

View file

@ -0,0 +1,43 @@
import { FormattedMessage } from 'react-intl';
import { useAppSelector } from 'mastodon/store';
import { TimelineHint } from './timeline_hint';
interface RemoteHintProps {
accountId?: string;
}
export const RemoteHint: React.FC<RemoteHintProps> = ({ accountId }) => {
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const domain = account?.acct ? account.acct.split('@')[1] : undefined;
if (
!account ||
!account.url ||
account.acct !== account.username ||
!domain
) {
return null;
}
return (
<TimelineHint
url={account.url}
message={
<FormattedMessage
id='hints.profiles.posts_may_be_missing'
defaultMessage='Some posts from this profile may be missing.'
/>
}
label={
<FormattedMessage
id='hints.profiles.see_more_posts'
defaultMessage='See more posts on {domain}'
values={{ domain: <strong>{domain}</strong> }}
/>
}
/>
);
};

View file

@ -25,7 +25,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../initial_state';
import { IconButton } from './icon_button';
@ -274,10 +274,9 @@ class StatusActionBar extends ImmutablePureComponent {
if (writtenByMe && pinnableStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
menu.push(null);
}
menu.push(null);
if (writtenByMe || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
@ -390,7 +389,7 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={bookmarkTitle} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
</div>
<div className='status__action-bar__button-wrapper'>
<DropdownMenuContainer
<Dropdown
scrollKey={scrollKey}
status={status}
items={menu}

View file

@ -115,6 +115,7 @@ class StatusContent extends PureComponent {
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
link.setAttribute('data-menu-hashtag', this.props.status.getIn(['account', 'id']));
} else {
link.setAttribute('title', link.href);
link.classList.add('unhandled-link');

View file

@ -1,50 +0,0 @@
import { connect } from 'react-redux';
import { fetchRelationships } from 'mastodon/actions/accounts';
import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
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.dropdownMenu.openId,
openedViaKeyboard: state.dropdownMenu.keyboard,
});
/**
* @param {any} dispatch
* @param {Object} root0
* @param {any} [root0.status]
* @param {any} root0.items
* @param {any} [root0.scrollKey]
*/
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
onOpen(id, onItemClick, keyboard) {
if (status) {
dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
}
dispatch(isUserTouching() ? openModal({
modalType: 'ACTIONS',
modalProps: {
status,
actions: items,
onClick: onItemClick,
},
}) : openDropdownMenu({ id, keyboard, scrollKey }));
},
onClose(id) {
dispatch(closeModal({
modalType: 'ACTIONS',
ignoreFocus: false,
}));
dispatch(closeDropdownMenu({ id }));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);

View file

@ -1,51 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Hashtag } from 'mastodon/components/hashtag';
const messages = defineMessages({
lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' },
});
class FeaturedTags extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.record,
featuredTags: ImmutablePropTypes.list,
tagged: PropTypes.string,
intl: PropTypes.object.isRequired,
};
render () {
const { account, featuredTags, intl } = this.props;
if (!account || account.get('suspended') || featuredTags.isEmpty()) {
return null;
}
return (
<div className='getting-started__trends'>
<h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4>
{featuredTags.take(3).map(featuredTag => (
<Hashtag
key={featuredTag.get('name')}
name={featuredTag.get('name')}
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
uses={featuredTag.get('statuses_count') * 1}
withGraph={false}
description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
/>
))}
</div>
);
}
}
export default injectIntl(FeaturedTags);

View file

@ -1,17 +0,0 @@
import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors';
import FeaturedTags from '../components/featured_tags';
const mapStateToProps = () => {
const getAccount = makeGetAccount();
return (state, { accountId }) => ({
account: getAccount(state, accountId),
featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()),
});
};
export default connect(mapStateToProps)(FeaturedTags);

View file

@ -1,52 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { connect } from 'react-redux';
import FeaturedTags from 'mastodon/features/account/containers/featured_tags_container';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
const mapStateToProps = (state, { match: { params: { acct } } }) => {
const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]);
if (!accountId) {
return {
isLoading: true,
};
}
return {
accountId,
isLoading: false,
};
};
class AccountNavigation extends PureComponent {
static propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
acct: PropTypes.string,
tagged: PropTypes.string,
}).isRequired,
}).isRequired,
accountId: PropTypes.string,
isLoading: PropTypes.bool,
};
render () {
const { accountId, isLoading, match: { params: { tagged } } } = this.props;
if (isLoading) {
return null;
}
return (
<FeaturedTags accountId={accountId} tagged={tagged} />
);
}
}
export default connect(mapStateToProps)(AccountNavigation);

View file

@ -0,0 +1,69 @@
import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router';
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
import { me } from 'mastodon/initial_state';
interface EmptyMessageProps {
suspended: boolean;
hidden: boolean;
blockedBy: boolean;
accountId?: string;
}
export const EmptyMessage: React.FC<EmptyMessageProps> = ({
accountId,
suspended,
hidden,
blockedBy,
}) => {
const { acct } = useParams<{ acct?: string }>();
if (!accountId) {
return null;
}
let message: React.ReactNode = null;
if (me === accountId) {
message = (
<FormattedMessage
id='empty_column.account_featured.me'
defaultMessage='You have not featured anything yet. Did you know that you can feature your posts, hashtags you use the most, and even your friends accounts on your profile?'
/>
);
} else if (suspended) {
message = (
<FormattedMessage
id='empty_column.account_suspended'
defaultMessage='Account suspended'
/>
);
} else if (hidden) {
message = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) {
message = (
<FormattedMessage
id='empty_column.account_unavailable'
defaultMessage='Profile unavailable'
/>
);
} else if (acct) {
message = (
<FormattedMessage
id='empty_column.account_featured.other'
defaultMessage='{acct} has not featured anything yet. Did you know that you can feature your posts, hashtags you use the most, and even your friends accounts on your profile?'
values={{ acct }}
/>
);
} else {
message = (
<FormattedMessage
id='empty_column.account_featured_other.unknown'
defaultMessage='This account has not featured anything yet.'
/>
);
}
return <div className='empty-column-indicator'>{message}</div>;
};

View file

@ -0,0 +1,52 @@
import { defineMessages, useIntl } from 'react-intl';
import type { Map as ImmutableMap } from 'immutable';
import { Hashtag } from 'mastodon/components/hashtag';
export type TagMap = ImmutableMap<
'id' | 'name' | 'url' | 'statuses_count' | 'last_status_at' | 'accountId',
string | null
>;
interface FeaturedTagProps {
tag: TagMap;
account: string;
}
const messages = defineMessages({
lastStatusAt: {
id: 'account.featured_tags.last_status_at',
defaultMessage: 'Last post on {date}',
},
empty: {
id: 'account.featured_tags.last_status_never',
defaultMessage: 'No posts',
},
});
export const FeaturedTag: React.FC<FeaturedTagProps> = ({ tag, account }) => {
const intl = useIntl();
const name = tag.get('name') ?? '';
const count = Number.parseInt(tag.get('statuses_count') ?? '');
return (
<Hashtag
key={name}
name={name}
to={`/@${account}/tagged/${name}`}
uses={count}
withGraph={false}
description={
count > 0
? intl.formatMessage(messages.lastStatusAt, {
date: intl.formatDate(tag.get('last_status_at') ?? '', {
month: 'short',
day: '2-digit',
year: 'numeric',
}),
})
: intl.formatMessage(messages.empty)
}
/>
);
};

View file

@ -0,0 +1,156 @@
import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router';
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RemoteHint } from 'mastodon/components/remote_hint';
import StatusContainer from 'mastodon/containers/status_container';
import { useAccountId } from 'mastodon/hooks/useAccountId';
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { AccountHeader } from '../account_timeline/components/account_header';
import Column from '../ui/components/column';
import { EmptyMessage } from './components/empty_message';
import { FeaturedTag } from './components/featured_tag';
import type { TagMap } from './components/featured_tag';
interface Params {
acct?: string;
id?: string;
}
const AccountFeatured = () => {
const accountId = useAccountId();
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
const forceEmptyState = suspended || blockedBy || hidden;
const { acct = '' } = useParams<Params>();
const dispatch = useAppDispatch();
useEffect(() => {
if (accountId) {
void dispatch(expandAccountFeaturedTimeline(accountId));
dispatch(fetchFeaturedTags(accountId));
}
}, [accountId, dispatch]);
const isLoading = useAppSelector(
(state) =>
!accountId ||
!!(state.timelines as ImmutableMap<string, unknown>).getIn([
`account:${accountId}:pinned`,
'isLoading',
]) ||
!!state.user_lists.getIn(['featured_tags', accountId, 'isLoading']),
);
const featuredTags = useAppSelector(
(state) =>
state.user_lists.getIn(
['featured_tags', accountId, 'items'],
ImmutableList(),
) as ImmutableList<TagMap>,
);
const featuredStatusIds = useAppSelector(
(state) =>
(state.timelines as ImmutableMap<string, unknown>).getIn(
[`account:${accountId}:pinned`, 'items'],
ImmutableList(),
) as ImmutableList<string>,
);
if (isLoading) {
return (
<AccountFeaturedWrapper accountId={accountId}>
<div className='scrollable__append'>
<LoadingIndicator />
</div>
</AccountFeaturedWrapper>
);
}
if (featuredStatusIds.isEmpty() && featuredTags.isEmpty()) {
return (
<AccountFeaturedWrapper accountId={accountId}>
<EmptyMessage
blockedBy={blockedBy}
hidden={hidden}
suspended={suspended}
accountId={accountId}
/>
<RemoteHint accountId={accountId} />
</AccountFeaturedWrapper>
);
}
return (
<Column>
<ColumnBackButton />
<div className='scrollable scrollable--flex'>
{accountId && (
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
)}
{!featuredTags.isEmpty() && (
<>
<h4 className='column-subheading'>
<FormattedMessage
id='account.featured.hashtags'
defaultMessage='Hashtags'
/>
</h4>
{featuredTags.map((tag) => (
<FeaturedTag key={tag.get('id')} tag={tag} account={acct} />
))}
</>
)}
{!featuredStatusIds.isEmpty() && (
<>
<h4 className='column-subheading'>
<FormattedMessage
id='account.featured.posts'
defaultMessage='Posts'
/>
</h4>
{featuredStatusIds.map((statusId) => (
<StatusContainer
key={`f-${statusId}`}
// @ts-expect-error inferred props are wrong
id={statusId}
contextType='account'
/>
))}
</>
)}
<RemoteHint accountId={accountId} />
</div>
</Column>
);
};
const AccountFeaturedWrapper = ({
children,
accountId,
}: React.PropsWithChildren<{ accountId?: string }>) => {
return (
<Column>
<ColumnBackButton />
<div className='scrollable scrollable--flex'>
{accountId && <AccountHeader accountId={accountId} />}
{children}
</div>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default AccountFeatured;

View file

@ -2,25 +2,22 @@ import { useEffect, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router-dom';
import { createSelector } from '@reduxjs/toolkit';
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { RemoteHint } from 'mastodon/components/remote_hint';
import ScrollableList from 'mastodon/components/scrollable_list';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import Column from 'mastodon/features/ui/components/column';
import { useAccountId } from 'mastodon/hooks/useAccountId';
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import type { RootState } from 'mastodon/store';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
@ -56,53 +53,11 @@ const getAccountGallery = createSelector(
},
);
interface Params {
acct?: string;
id?: string;
}
const RemoteHint: React.FC<{
accountId: string;
}> = ({ accountId }) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
const acct = account?.acct;
const url = account?.url;
const domain = acct ? acct.split('@')[1] : undefined;
if (!url) {
return null;
}
return (
<TimelineHint
url={url}
message={
<FormattedMessage
id='hints.profiles.posts_may_be_missing'
defaultMessage='Some posts from this profile may be missing.'
/>
}
label={
<FormattedMessage
id='hints.profiles.see_more_posts'
defaultMessage='See more posts on {domain}'
values={{ domain: <strong>{domain}</strong> }}
/>
}
/>
);
};
export const AccountGallery: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
const { acct, id } = useParams<Params>();
const dispatch = useAppDispatch();
const accountId = useAppSelector(
(state) =>
id ??
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
);
const accountId = useAccountId();
const attachments = useAppSelector((state) =>
accountId
? getAccountGallery(state, accountId)
@ -123,33 +78,15 @@ export const AccountGallery: React.FC<{
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const blockedBy = useAppSelector(
(state) =>
state.relationships.getIn([accountId, 'blocked_by'], false) as boolean,
);
const suspended = useAppSelector(
(state) => state.accounts.getIn([accountId, 'suspended'], false) as boolean,
);
const isAccount = !!account;
const remote = account?.acct !== account?.username;
const hidden = useAppSelector((state) =>
accountId ? getAccountHidden(state, accountId) : false,
);
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
const maxId = attachments.last()?.getIn(['status', 'id']) as
| string
| undefined;
useEffect(() => {
if (!accountId) {
dispatch(lookupAccount(acct));
}
}, [dispatch, accountId, acct]);
useEffect(() => {
if (accountId && !isAccount) {
dispatch(fetchAccount(accountId));
}
if (accountId && isAccount) {
void dispatch(expandAccountMediaTimeline(accountId));
}
@ -233,7 +170,7 @@ export const AccountGallery: React.FC<{
defaultMessage='Profile unavailable'
/>
);
} else if (remote && attachments.isEmpty()) {
} else if (attachments.isEmpty()) {
emptyMessage = <RemoteHint accountId={accountId} />;
} else {
emptyMessage = (
@ -259,7 +196,7 @@ export const AccountGallery: React.FC<{
)
}
alwaysPrepend
append={remote && accountId && <RemoteHint accountId={accountId} />}
append={accountId && <RemoteHint accountId={accountId} />}
scrollKey='account_gallery'
isLoading={isLoading}
hasMore={!forceEmptyState && hasMore}

View file

@ -18,6 +18,7 @@ import {
unmuteAccount,
pinAccount,
unpinAccount,
removeAccountFromFollowers,
} from 'mastodon/actions/accounts';
import { initBlockModal } from 'mastodon/actions/blocks';
import { mentionCompose, directCompose } from 'mastodon/actions/compose';
@ -37,12 +38,12 @@ import {
FollowingCounter,
StatusesCounter,
} from 'mastodon/components/counters';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { FollowButton } from 'mastodon/components/follow_button';
import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
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 { DomainPill } from 'mastodon/features/account/components/domain_pill';
import AccountNoteContainer from 'mastodon/features/account/containers/account_note_container';
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
@ -50,8 +51,7 @@ import { useLinks } from 'mastodon/hooks/useLinks';
import { useIdentity } from 'mastodon/identity_context';
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import type { DropdownMenu } from 'mastodon/models/dropdown_menu';
import type { Relationship } from 'mastodon/models/relationship';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import {
PERMISSION_MANAGE_USERS,
PERMISSION_MANAGE_FEDERATION,
@ -63,18 +63,6 @@ import { MemorialNote } from './memorial_note';
import { MovedNote } from './moved_note';
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: {
@ -165,6 +153,23 @@ const messages = defineMessages({
id: 'account.open_original_page',
defaultMessage: 'Open original page',
},
removeFromFollowers: {
id: 'account.remove_from_followers',
defaultMessage: 'Remove {name} from followers',
},
confirmRemoveFromFollowersTitle: {
id: 'confirmations.remove_from_followers.title',
defaultMessage: 'Remove follower?',
},
confirmRemoveFromFollowersMessage: {
id: 'confirmations.remove_from_followers.message',
defaultMessage:
'{name} will stop following you. Are you sure you want to proceed?',
},
confirmRemoveFromFollowersButton: {
id: 'confirmations.remove_from_followers.confirm',
defaultMessage: 'Remove follower',
},
});
const titleFromAccount = (account: Account) => {
@ -179,20 +184,6 @@ const titleFromAccount = (account: Account) => {
return `${prefix} (@${acct})`;
};
const messageForFollowButton = (relationship?: 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: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
@ -215,20 +206,6 @@ export const AccountHeader: React.FC<{
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
const handleLinkClick = useLinks();
const handleFollow = useCallback(() => {
if (!account) {
return;
}
if (relationship?.following || relationship?.requested) {
dispatch(
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
);
} else {
dispatch(followAccount(account.id));
}
}, [dispatch, account, relationship]);
const handleBlock = useCallback(() => {
if (!account) {
return;
@ -365,23 +342,6 @@ export const AccountHeader: React.FC<{
);
}, [dispatch, account]);
const handleInteractionModal = useCallback(() => {
if (!account) {
return;
}
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'follow',
accountId: account.id,
url: account.uri,
},
}),
);
}, [dispatch, account]);
const handleOpenAvatar = useCallback(
(e: React.MouseEvent) => {
if (e.button !== 0 || e.ctrlKey || e.metaKey) {
@ -417,10 +377,6 @@ export const AccountHeader: React.FC<{
});
}, [account]);
const handleEditProfile = useCallback(() => {
window.open('/settings/profile', '_blank');
}, []);
const handleMouseEnter = useCallback(
({ currentTarget }: React.MouseEvent) => {
if (autoPlayGif) {
@ -456,7 +412,7 @@ export const AccountHeader: React.FC<{
const remoteDomain = isRemote ? account?.acct.split('@')[1] : null;
const menu = useMemo(() => {
const arr: DropdownMenu = [];
const arr: MenuItem[] = [];
if (!account) {
return arr;
@ -545,9 +501,7 @@ export const AccountHeader: React.FC<{
arr.push({
text: intl.formatMessage(
account.getIn(['relationship', 'endorsed'])
? messages.unendorse
: messages.endorse,
relationship.endorsed ? messages.unendorse : messages.endorse,
),
action: handleEndorseToggle,
});
@ -558,6 +512,39 @@ export const AccountHeader: React.FC<{
arr.push(null);
}
if (relationship?.followed_by) {
const handleRemoveFromFollowers = () => {
dispatch(
openModal({
modalType: 'CONFIRM',
modalProps: {
title: intl.formatMessage(
messages.confirmRemoveFromFollowersTitle,
),
message: intl.formatMessage(
messages.confirmRemoveFromFollowersMessage,
{ name: <strong>{account.acct}</strong> },
),
confirm: intl.formatMessage(
messages.confirmRemoveFromFollowersButton,
),
onConfirm: () => {
void dispatch(removeAccountFromFollowers({ accountId }));
},
},
}),
);
};
arr.push({
text: intl.formatMessage(messages.removeFromFollowers, {
name: account.username,
}),
action: handleRemoveFromFollowers,
dangerous: true,
});
}
if (relationship?.muting) {
arr.push({
text: intl.formatMessage(messages.unmute, {
@ -656,6 +643,8 @@ export const AccountHeader: React.FC<{
return arr;
}, [
dispatch,
accountId,
account,
relationship,
permissions,
@ -680,33 +669,72 @@ export const AccountHeader: React.FC<{
return null;
}
let actionBtn, bellBtn, lockedIcon, shareBtn;
let actionBtn: React.ReactNode,
bellBtn: React.ReactNode,
lockedIcon: React.ReactNode,
shareBtn: React.ReactNode;
const info = [];
const info: React.ReactNode[] = [];
if (me !== account.id && relationship?.blocking) {
info.push(
<span key='blocked' className='relationship-tag'>
<FormattedMessage id='account.blocked' defaultMessage='Blocked' />
</span>,
);
}
if (me !== account.id && relationship) {
if (
relationship.followed_by &&
(relationship.following || relationship.requested)
) {
info.push(
<span key='mutual' className='relationship-tag'>
<FormattedMessage
id='account.mutual'
defaultMessage='You follow each other'
/>
</span>,
);
} else if (relationship.followed_by) {
info.push(
<span key='followed_by' className='relationship-tag'>
<FormattedMessage
id='account.follows_you'
defaultMessage='Follows you'
/>
</span>,
);
} else if (relationship.requested_by) {
info.push(
<span key='requested_by' className='relationship-tag'>
<FormattedMessage
id='account.requests_to_follow_you'
defaultMessage='Requests to follow you'
/>
</span>,
);
}
if (me !== account.id && relationship?.muting) {
info.push(
<span key='muted' className='relationship-tag'>
<FormattedMessage id='account.muted' defaultMessage='Muted' />
</span>,
);
} else if (me !== account.id && relationship?.domain_blocking) {
info.push(
<span key='domain_blocked' className='relationship-tag'>
<FormattedMessage
id='account.domain_blocked'
defaultMessage='Domain blocked'
/>
</span>,
);
if (relationship.blocking) {
info.push(
<span key='blocking' className='relationship-tag'>
<FormattedMessage id='account.blocking' defaultMessage='Blocking' />
</span>,
);
}
if (relationship.muting) {
info.push(
<span key='muting' className='relationship-tag'>
<FormattedMessage id='account.muting' defaultMessage='Muting' />
</span>,
);
}
if (relationship.domain_blocking) {
info.push(
<span key='domain_blocking' className='relationship-tag'>
<FormattedMessage
id='account.domain_blocking'
defaultMessage='Blocking domain'
/>
</span>,
);
}
}
if (relationship?.requested || relationship?.following) {
@ -750,43 +778,17 @@ export const AccountHeader: React.FC<{
);
}
if (me !== account.id) {
if (signedIn && !relationship) {
// Wait until the relationship is loaded
actionBtn = (
<Button disabled>
<LoadingIndicator />
</Button>
);
} else if (!relationship?.blocking) {
actionBtn = (
<Button
disabled={relationship?.blocked_by}
className={classNames({
'button--destructive':
relationship?.following || relationship?.requested,
})}
text={intl.formatMessage(messageForFollowButton(relationship))}
onClick={signedIn ? handleFollow : handleInteractionModal}
/>
);
} else {
actionBtn = (
<Button
text={intl.formatMessage(messages.unblock, {
name: account.username,
})}
onClick={handleBlock}
/>
);
}
} else {
if (relationship?.blocking) {
actionBtn = (
<Button
text={intl.formatMessage(messages.edit_profile)}
onClick={handleEditProfile}
text={intl.formatMessage(messages.unblock, {
name: account.username,
})}
onClick={handleBlock}
/>
);
} else {
actionBtn = <FollowButton accountId={accountId} />;
}
if (account.moved && !relationship?.following) {
@ -819,7 +821,7 @@ export const AccountHeader: React.FC<{
badges.push(<GroupBadge key='group-badge' />);
}
account.get('roles', []).forEach((role) => {
account.roles.forEach((role) => {
badges.push(
<Badge
key={`role-badge-${role.get('id')}`}
@ -879,13 +881,11 @@ export const AccountHeader: React.FC<{
<div className='account__header__tabs__buttons'>
{!hidden && bellBtn}
{!hidden && shareBtn}
<DropdownMenuContainer
<Dropdown
disabled={menu.length === 0}
items={menu}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
size={24}
direction='right'
/>
{!hidden && actionBtn}
</div>
@ -1031,6 +1031,9 @@ export const AccountHeader: React.FC<{
{!(hideTabs || hidden) && (
<div className='account__section-headline'>
<NavLink exact to={`/@${account.acct}/featured`}>
<FormattedMessage id='account.featured' defaultMessage='Featured' />
</NavLink>
<NavLink exact to={`/@${account.acct}`}>
<FormattedMessage id='account.posts' defaultMessage='Posts' />
</NavLink>

View file

@ -7,12 +7,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
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/accounts';
import { useAppSelector } from 'mastodon/store';
import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags';
@ -21,6 +19,7 @@ import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import StatusList from '../../components/status_list';
import Column from '../ui/components/column';
import { RemoteHint } from 'mastodon/components/remote_hint';
import { AccountHeader } from './components/account_header';
import { LimitedAccountHint } from './components/limited_account_hint';
@ -47,11 +46,8 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
return {
accountId,
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], emptyList),
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
@ -60,24 +56,6 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
};
};
const RemoteHint = ({ accountId, url }) => {
const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.posts_may_be_missing' defaultMessage='Some posts from this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_posts' defaultMessage='See more posts on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = {
url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
};
class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
@ -89,7 +67,6 @@ class AccountTimeline extends ImmutablePureComponent {
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
withReplies: PropTypes.bool,
@ -97,8 +74,6 @@ class AccountTimeline extends ImmutablePureComponent {
isAccount: PropTypes.bool,
suspended: PropTypes.bool,
hidden: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
};
@ -161,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
};
render () {
const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
const { accountId, statusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
if (isLoading && statusIds.isEmpty()) {
return (
@ -191,8 +166,6 @@ class AccountTimeline extends ImmutablePureComponent {
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
}
const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return (
<Column>
<ColumnBackButton />
@ -200,10 +173,9 @@ class AccountTimeline extends ImmutablePureComponent {
<StatusList
prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
alwaysPrepend
append={remoteMessage}
append={<RemoteHint accountId={accountId} />}
scrollKey='account_timeline'
statusIds={forceEmptyState ? emptyList : statusIds}
featuredStatusIds={featuredStatusIds}
isLoading={isLoading}
hasMore={!forceEmptyState && hasMore}
onLoadMore={this.handleLoadMore}

View file

@ -2,7 +2,6 @@ import {
useState,
useCallback,
useRef,
useEffect,
useImperativeHandle,
forwardRef,
} from 'react';
@ -13,6 +12,7 @@ import classNames from 'classnames';
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { useSpring, animated } from '@react-spring/web';
import Textarea from 'react-textarea-autosize';
import { length } from 'stringz';
// eslint-disable-next-line import/extensions
@ -31,7 +31,7 @@ import Audio from 'mastodon/features/audio';
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import { Video, getPointerPosition } from 'mastodon/features/video';
import { me } from 'mastodon/initial_state';
import { me, reduceMotion } from 'mastodon/initial_state';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { assetHost } from 'mastodon/utils/config';
@ -105,6 +105,17 @@ const Preview: React.FC<{
position: FocalPoint;
onPositionChange: (arg0: FocalPoint) => void;
}> = ({ mediaId, position, onPositionChange }) => {
const draggingRef = useRef<boolean>(false);
const nodeRef = useRef<HTMLImageElement | HTMLVideoElement | null>(null);
const [x, y] = position;
const style = useSpring({
to: {
left: `${x * 100}%`,
top: `${y * 100}%`,
},
immediate: reduceMotion || draggingRef.current,
});
const media = useAppSelector((state) =>
(
(state.compose as ImmutableMap<string, unknown>).get(
@ -117,9 +128,6 @@ const Preview: React.FC<{
);
const [dragging, setDragging] = useState(false);
const [x, y] = position;
const nodeRef = useRef<HTMLImageElement | HTMLVideoElement | null>(null);
const draggingRef = useRef<boolean>(false);
const setRef = useCallback(
(e: HTMLImageElement | HTMLVideoElement | null) => {
@ -134,36 +142,30 @@ const Preview: React.FC<{
return;
}
const handleMouseMove = (e: MouseEvent) => {
const { x, y } = getPointerPosition(nodeRef.current, e);
draggingRef.current = true; // This will disable the animation for quicker feedback, only do this if the mouse actually moves
onPositionChange([x, y]);
};
const handleMouseUp = () => {
setDragging(false);
draggingRef.current = false;
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
const { x, y } = getPointerPosition(nodeRef.current, e.nativeEvent);
setDragging(true);
draggingRef.current = true;
onPositionChange([x, y]);
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
},
[setDragging, onPositionChange],
);
useEffect(() => {
const handleMouseUp = () => {
setDragging(false);
draggingRef.current = false;
};
const handleMouseMove = (e: MouseEvent) => {
if (draggingRef.current) {
const { x, y } = getPointerPosition(nodeRef.current, e);
onPositionChange([x, y]);
}
};
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
}, [setDragging, onPositionChange]);
if (!media) {
return null;
}
@ -179,10 +181,7 @@ const Preview: React.FC<{
role='presentation'
onMouseDown={handleMouseDown}
/>
<div
className='focal-point__reticle'
style={{ top: `${y * 100}%`, left: `${x * 100}%` }}
/>
<animated.div className='focal-point__reticle' style={style} />
</div>
);
} else if (media.get('type') === 'gifv') {
@ -194,10 +193,7 @@ const Preview: React.FC<{
alt=''
onMouseDown={handleMouseDown}
/>
<div
className='focal-point__reticle'
style={{ top: `${y * 100}%`, left: `${x * 100}%` }}
/>
<animated.div className='focal-point__reticle' style={style} />
</div>
);
} else if (media.get('type') === 'video') {

View file

@ -1,115 +0,0 @@
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 BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'mastodon/actions/bookmarks';
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
import ColumnHeader from 'mastodon/components/column_header';
import StatusList from 'mastodon/components/status_list';
import Column from 'mastodon/features/ui/components/column';
import { getStatusList } from 'mastodon/selectors';
const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
});
const mapStateToProps = state => ({
statusIds: getStatusList(state, 'bookmarks'),
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
});
class Bookmarks extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
UNSAFE_componentWillMount () {
this.props.dispatch(fetchBookmarkedStatuses());
}
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('BOOKMARKS', {}));
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
};
handleHeaderClick = () => {
this.column.scrollTop();
};
setRef = c => {
this.column = c;
};
handleLoadMore = debounce(() => {
this.props.dispatch(expandBookmarkedStatuses());
}, 300, { leading: true });
render () {
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here." />;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader
icon='bookmarks'
iconComponent={BookmarksIcon}
title={intl.formatMessage(messages.heading)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
/>
<StatusList
trackScroll={!pinned}
statusIds={statusIds}
scrollKey={`bookmarked_statuses-${columnId}`}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='bookmarks'
/>
<Helmet>
<title>{intl.formatMessage(messages.heading)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default connect(mapStateToProps)(injectIntl(Bookmarks));

View file

@ -0,0 +1,116 @@
import { useEffect, useRef, useCallback } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
import {
fetchBookmarkedStatuses,
expandBookmarkedStatuses,
} from 'mastodon/actions/bookmarks';
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
import { Column } from 'mastodon/components/column';
import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import StatusList from 'mastodon/components/status_list';
import { getStatusList } from 'mastodon/selectors';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
});
const Bookmarks: React.FC<{
columnId: string;
multiColumn: boolean;
}> = ({ columnId, multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const columnRef = useRef<ColumnRef>(null);
const statusIds = useAppSelector((state) =>
getStatusList(state, 'bookmarks'),
);
const isLoading = useAppSelector(
(state) =>
state.status_lists.getIn(['bookmarks', 'isLoading'], true) as boolean,
);
const hasMore = useAppSelector(
(state) => !!state.status_lists.getIn(['bookmarks', 'next']),
);
useEffect(() => {
dispatch(fetchBookmarkedStatuses());
}, [dispatch]);
const handlePin = useCallback(() => {
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('BOOKMARKS', {}));
}
}, [dispatch, columnId]);
const handleMove = useCallback(
(dir: number) => {
dispatch(moveColumn(columnId, dir));
},
[dispatch, columnId],
);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);
const handleLoadMore = useCallback(() => {
dispatch(expandBookmarkedStatuses());
}, [dispatch]);
const pinned = !!columnId;
const emptyMessage = (
<FormattedMessage
id='empty_column.bookmarked_statuses'
defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here."
/>
);
return (
<Column
bindToDocument={!multiColumn}
ref={columnRef}
label={intl.formatMessage(messages.heading)}
>
<ColumnHeader
icon='bookmarks'
iconComponent={BookmarksIcon}
title={intl.formatMessage(messages.heading)}
onPin={handlePin}
onMove={handleMove}
onClick={handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
/>
<StatusList
trackScroll={!pinned}
statusIds={statusIds}
scrollKey={`bookmarked_statuses-${columnId}`}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='bookmarks'
/>
<Helmet>
<title>{intl.formatMessage(messages.heading)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default Bookmarks;

View file

@ -2,64 +2,82 @@ import { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'mastodon/actions/modal';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
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' },
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' },
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' },
domain_blocks: {
id: 'navigation_bar.domain_blocks',
defaultMessage: 'Blocked domains',
},
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
});
export const ActionBar = () => {
const dispatch = useDispatch();
export const ActionBar: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const menu = useMemo(() => {
const handleLogoutClick = () => {
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} }));
};
return ([
{ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' },
{ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' },
return [
{
text: intl.formatMessage(messages.edit_profile),
href: '/settings/profile',
},
{
text: intl.formatMessage(messages.preferences),
href: '/settings/preferences',
},
{ text: intl.formatMessage(messages.pins), to: '/pinned' },
null,
{ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' },
{
text: intl.formatMessage(messages.follow_requests),
to: '/follow_requests',
},
{ text: intl.formatMessage(messages.favourites), to: '/favourites' },
{ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' },
{ text: intl.formatMessage(messages.lists), to: '/lists' },
{ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' },
{
text: intl.formatMessage(messages.followed_tags),
to: '/followed_tags',
},
null,
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
{ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' },
{
text: intl.formatMessage(messages.domain_blocks),
to: '/domain_blocks',
},
{ text: intl.formatMessage(messages.filters), href: '/filters' },
null,
{ text: intl.formatMessage(messages.logout), action: handleLogoutClick },
]);
];
}, [intl, dispatch]);
return (
<DropdownMenuContainer
items={menu}
icon='bars'
iconComponent={MoreHorizIcon}
size={24}
direction='right'
/>
);
return <Dropdown items={menu} icon='bars' iconComponent={MoreHorizIcon} />;
};

View file

@ -24,7 +24,7 @@ import AvatarComposite from 'mastodon/components/avatar_composite';
import { IconButton } from 'mastodon/components/icon_button';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import StatusContent from 'mastodon/components/status_content';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { autoPlayGif } from 'mastodon/initial_state';
import { makeGetStatus } from 'mastodon/selectors';
@ -205,7 +205,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={handleReply} />
<div className='status__action-bar-dropdown'>
<DropdownMenuContainer
<Dropdown
scrollKey={scrollKey}
status={lastStatus}
items={menu}

View file

@ -25,7 +25,7 @@ export const Card = ({ id, source }) => {
const dispatch = useDispatch();
const handleDismiss = useCallback(() => {
dispatch(dismissSuggestion(id));
dispatch(dismissSuggestion({ accountId: id }));
}, [id, dispatch]);
let label;

View file

@ -1,115 +0,0 @@
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 StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'mastodon/actions/favourites';
import ColumnHeader from 'mastodon/components/column_header';
import StatusList from 'mastodon/components/status_list';
import Column from 'mastodon/features/ui/components/column';
import { getStatusList } from 'mastodon/selectors';
const messages = defineMessages({
heading: { id: 'column.favourites', defaultMessage: 'Favorites' },
});
const mapStateToProps = state => ({
statusIds: getStatusList(state, 'favourites'),
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
});
class Favourites extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
UNSAFE_componentWillMount () {
this.props.dispatch(fetchFavouritedStatuses());
}
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('FAVOURITES', {}));
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
};
handleHeaderClick = () => {
this.column.scrollTop();
};
setRef = c => {
this.column = c;
};
handleLoadMore = debounce(() => {
this.props.dispatch(expandFavouritedStatuses());
}, 300, { leading: true });
render () {
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favorite posts yet. When you favorite one, it will show up here." />;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader
icon='star'
iconComponent={StarIcon}
title={intl.formatMessage(messages.heading)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
/>
<StatusList
trackScroll={!pinned}
statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='favourites'
/>
<Helmet>
<title>{intl.formatMessage(messages.heading)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default connect(mapStateToProps)(injectIntl(Favourites));

View file

@ -0,0 +1,116 @@
import { useEffect, useRef, useCallback } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
import {
fetchFavouritedStatuses,
expandFavouritedStatuses,
} from 'mastodon/actions/favourites';
import { Column } from 'mastodon/components/column';
import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import StatusList from 'mastodon/components/status_list';
import { getStatusList } from 'mastodon/selectors';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
heading: { id: 'column.favourites', defaultMessage: 'Favorites' },
});
const Favourites: React.FC<{ columnId: string; multiColumn: boolean }> = ({
columnId,
multiColumn,
}) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const columnRef = useRef<ColumnRef>(null);
const statusIds = useAppSelector((state) =>
getStatusList(state, 'favourites'),
);
const isLoading = useAppSelector(
(state) =>
state.status_lists.getIn(['favourites', 'isLoading'], true) as boolean,
);
const hasMore = useAppSelector(
(state) => !!state.status_lists.getIn(['favourites', 'next']),
);
useEffect(() => {
dispatch(fetchFavouritedStatuses());
}, [dispatch]);
const handlePin = useCallback(() => {
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('FAVOURITES', {}));
}
}, [dispatch, columnId]);
const handleMove = useCallback(
(dir: number) => {
dispatch(moveColumn(columnId, dir));
},
[dispatch, columnId],
);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);
const handleLoadMore = useCallback(() => {
dispatch(expandFavouritedStatuses());
}, [dispatch]);
const pinned = !!columnId;
const emptyMessage = (
<FormattedMessage
id='empty_column.favourited_statuses'
defaultMessage="You don't have any favorite posts yet. When you favorite one, it will show up here."
/>
);
return (
<Column
bindToDocument={!multiColumn}
ref={columnRef}
label={intl.formatMessage(messages.heading)}
>
<ColumnHeader
icon='star'
iconComponent={StarIcon}
title={intl.formatMessage(messages.heading)}
onPin={handlePin}
onMove={handleMove}
onClick={handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
/>
<StatusList
trackScroll={!pinned}
statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='favourites'
/>
<Helmet>
<title>{intl.formatMessage(messages.heading)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default Favourites;

View file

@ -1,95 +0,0 @@
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 TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags';
import ColumnHeader from 'mastodon/components/column_header';
import { Hashtag } from 'mastodon/components/hashtag';
import ScrollableList from 'mastodon/components/scrollable_list';
import Column from 'mastodon/features/ui/components/column';
const messages = defineMessages({
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
});
const mapStateToProps = state => ({
hashtags: state.getIn(['followed_tags', 'items']),
isLoading: state.getIn(['followed_tags', 'isLoading'], true),
hasMore: !!state.getIn(['followed_tags', 'next']),
});
class FollowedTags extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hashtags: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
multiColumn: PropTypes.bool,
};
componentDidMount() {
this.props.dispatch(fetchFollowedHashtags());
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowedHashtags());
}, 300, { leading: true });
render () {
const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props;
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.' />;
return (
<Column bindToDocument={!multiColumn}>
<ColumnHeader
icon='hashtag'
iconComponent={TagIcon}
title={intl.formatMessage(messages.heading)}
showBackButton
multiColumn={multiColumn}
/>
<ScrollableList
scrollKey='followed_tags'
emptyMessage={emptyMessage}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
bindToDocument={!multiColumn}
>
{hashtags.map((hashtag) => (
<Hashtag
key={hashtag.get('name')}
name={hashtag.get('name')}
to={`/tags/${hashtag.get('name')}`}
withGraph={false}
// Taken from ImmutableHashtag. Should maybe refactor ImmutableHashtag to accept more options?
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
/>
))}
</ScrollableList>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default connect(mapStateToProps)(injectIntl(FollowedTags));

View file

@ -0,0 +1,161 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { isFulfilled } from '@reduxjs/toolkit';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { unfollowHashtag } from 'mastodon/actions/tags_typed';
import { apiGetFollowedTags } from 'mastodon/api/tags';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import { Button } from 'mastodon/components/button';
import { Column } from 'mastodon/components/column';
import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Hashtag } from 'mastodon/components/hashtag';
import ScrollableList from 'mastodon/components/scrollable_list';
import { useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
});
const FollowedTag: React.FC<{
tag: ApiHashtagJSON;
onUnfollow: (arg0: string) => void;
}> = ({ tag, onUnfollow }) => {
const dispatch = useAppDispatch();
const tagId = tag.name;
const handleClick = useCallback(() => {
void dispatch(unfollowHashtag({ tagId })).then((result) => {
if (isFulfilled(result)) {
onUnfollow(tagId);
}
return '';
});
}, [dispatch, onUnfollow, tagId]);
const people =
parseInt(tag.history[0].accounts) +
parseInt(tag.history[1]?.accounts ?? '');
return (
<Hashtag
name={tag.name}
to={`/tags/${tag.name}`}
withGraph={false}
people={people}
>
<Button onClick={handleClick}>
<FormattedMessage id='account.unfollow' defaultMessage='Unfollow' />
</Button>
</Hashtag>
);
};
const FollowedTags: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const intl = useIntl();
const [tags, setTags] = useState<ApiHashtagJSON[]>([]);
const [loading, setLoading] = useState(false);
const [next, setNext] = useState<string | undefined>();
const hasMore = !!next;
const columnRef = useRef<ColumnRef>(null);
useEffect(() => {
setLoading(true);
void apiGetFollowedTags()
.then(({ tags, links }) => {
const next = links.refs.find((link) => link.rel === 'next');
setTags(tags);
setLoading(false);
setNext(next?.uri);
return '';
})
.catch(() => {
setLoading(false);
});
}, [setTags, setLoading, setNext]);
const handleLoadMore = useCallback(() => {
setLoading(true);
void apiGetFollowedTags(next)
.then(({ tags, links }) => {
const next = links.refs.find((link) => link.rel === 'next');
setLoading(false);
setTags((previousTags) => [...previousTags, ...tags]);
setNext(next?.uri);
return '';
})
.catch(() => {
setLoading(false);
});
}, [setTags, setLoading, setNext, next]);
const handleUnfollow = useCallback(
(tagId: string) => {
setTags((tags) => tags.filter((tag) => tag.name !== tagId));
},
[setTags],
);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);
const emptyMessage = (
<FormattedMessage
id='empty_column.followed_tags'
defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.'
/>
);
return (
<Column
bindToDocument={!multiColumn}
ref={columnRef}
label={intl.formatMessage(messages.heading)}
>
<ColumnHeader
icon='hashtag'
iconComponent={TagIcon}
title={intl.formatMessage(messages.heading)}
onClick={handleHeaderClick}
multiColumn={multiColumn}
showBackButton
/>
<ScrollableList
scrollKey='followed_tags'
emptyMessage={emptyMessage}
hasMore={hasMore}
isLoading={loading}
showLoading={loading && tags.length === 0}
onLoadMore={handleLoadMore}
trackScroll={!multiColumn}
bindToDocument={!multiColumn}
>
{tags.map((tag) => (
<FollowedTag key={tag.name} tag={tag} onUnfollow={handleUnfollow} />
))}
</ScrollableList>
<Helmet>
<title>{intl.formatMessage(messages.heading)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default FollowedTags;

View file

@ -12,8 +12,8 @@ import {
} from 'mastodon/actions/tags_typed';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import { Button } from 'mastodon/components/button';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenu from 'mastodon/containers/dropdown_menu_container';
import { useIdentity } from 'mastodon/identity_context';
import { PERMISSION_MANAGE_TAXONOMIES } from 'mastodon/permissions';
import { useAppDispatch } from 'mastodon/store';
@ -153,13 +153,11 @@ export const HashtagHeader: React.FC<{
<div className='hashtag-header__header__buttons'>
{menu.length > 0 && (
<DropdownMenu
<Dropdown
disabled={menu.length === 0}
items={menu}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
size={24}
direction='right'
/>
)}

View file

@ -13,9 +13,9 @@ import { fetchLists } from 'mastodon/actions/lists';
import { openModal } from 'mastodon/actions/modal';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { getOrderedLists } from 'mastodon/selectors/lists';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
@ -60,12 +60,11 @@ const ListItem: React.FC<{
<span>{title}</span>
</Link>
<DropdownMenuContainer
<Dropdown
scrollKey='lists'
items={menu}
icons='ellipsis-h'
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
</div>

View file

@ -17,7 +17,7 @@ import { initReport } from 'mastodon/actions/reports';
import { Avatar } from 'mastodon/components/avatar';
import { CheckBox } from 'mastodon/components/check_box';
import { IconButton } from 'mastodon/components/icon_button';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { makeGetAccount } from 'mastodon/selectors';
import { toCappedNumber } from 'mastodon/utils/numbers';
@ -105,11 +105,10 @@ export const NotificationRequest = ({ id, accountId, notificationsCount, checked
<div className='notification-request__actions'>
<IconButton iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<DropdownMenuContainer
<Dropdown
items={menu}
icons='ellipsis-h'
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
</div>

View file

@ -23,7 +23,7 @@ import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { NotificationRequest } from './components/notification_request';
import { PolicyControls } from './components/policy_controls';
@ -126,7 +126,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
<div className='column-header__select-row__checkbox'>
<CheckBox checked={selectAllChecked} indeterminate={selectedCount > 0 && !selectAllChecked} onChange={handleSelectAll} />
</div>
<DropdownMenuContainer
<Dropdown
items={menu}
icons='ellipsis-h'
iconComponent={MoreHorizIcon}
@ -139,7 +139,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
</span>
<Icon id='down' icon={ArrowDropDownIcon} />
</button>
</DropdownMenuContainer>
</Dropdown>
<div className='column-header__select-row__mode-button'>
<button className='text-btn' tabIndex={0} onClick={handleToggleSelectionMode}>
{selectionMode ? (

View file

@ -24,7 +24,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { IconButton } from '../../../components/icon_button';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../../../initial_state';
const messages = defineMessages({
@ -317,7 +317,7 @@ class ActionBar extends PureComponent {
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={bookmarkTitle} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer icon='ellipsis-h' iconComponent={MoreHorizIcon} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
<Dropdown icon='ellipsis-h' iconComponent={MoreHorizIcon} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
</div>
</div>
);

View file

@ -14,7 +14,7 @@ import { Link } from 'react-router-dom';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import { AnimatedNumber } from 'mastodon/components/animated_number';
import { ContentWarning } from 'mastodon/components/content_warning';
import EditedTimestamp from 'mastodon/components/edited_timestamp';
import { EditedTimestamp } from 'mastodon/components/edited_timestamp';
import { FilterWarning } from 'mastodon/components/filter_warning';
import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
import type { StatusLike } from 'mastodon/components/hashtag_bar';

View file

@ -0,0 +1,157 @@
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import { useLocation } from 'react-router-dom';
import Overlay from 'react-overlays/Overlay';
import type {
OffsetValue,
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
import { DropdownMenu } from 'mastodon/components/dropdown_menu';
import { useAppSelector } from 'mastodon/store';
const messages = defineMessages({
browseHashtag: {
id: 'hashtag.browse',
defaultMessage: 'Browse posts in #{hashtag}',
},
browseHashtagFromAccount: {
id: 'hashtag.browse_from_account',
defaultMessage: 'Browse posts from @{name} in #{hashtag}',
},
muteHashtag: { id: 'hashtag.mute', defaultMessage: 'Mute #{hashtag}' },
});
const offset = [5, 5] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
const isHashtagLink = (
element: HTMLAnchorElement | null,
): element is HTMLAnchorElement => {
if (!element) {
return false;
}
return element.matches('[data-menu-hashtag]');
};
interface TargetParams {
hashtag?: string;
accountId?: string;
}
export const HashtagMenuController: React.FC = () => {
const intl = useIntl();
const [open, setOpen] = useState(false);
const [{ accountId, hashtag }, setTargetParams] = useState<TargetParams>({});
const targetRef = useRef<HTMLAnchorElement | null>(null);
const location = useLocation();
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
useEffect(() => {
setOpen(false);
targetRef.current = null;
}, [setOpen, location]);
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const target = (e.target as HTMLElement).closest('a');
if (e.button !== 0 || e.ctrlKey || e.metaKey) {
return;
}
if (!isHashtagLink(target)) {
return;
}
const hashtag = target.text.replace(/^#/, '');
const accountId = target.getAttribute('data-menu-hashtag');
if (!hashtag || !accountId) {
return;
}
e.preventDefault();
e.stopPropagation();
targetRef.current = target;
setOpen(true);
setTargetParams({ hashtag, accountId });
};
document.addEventListener('click', handleClick, { capture: true });
return () => {
document.removeEventListener('click', handleClick);
};
}, [setTargetParams, setOpen]);
const handleClose = useCallback(() => {
setOpen(false);
targetRef.current = null;
}, [setOpen]);
const menu = useMemo(
() => [
{
text: intl.formatMessage(messages.browseHashtag, {
hashtag,
}),
to: `/tags/${hashtag}`,
},
{
text: intl.formatMessage(messages.browseHashtagFromAccount, {
hashtag,
name: account?.username,
}),
to: `/@${account?.acct}/tagged/${hashtag}`,
},
null,
{
text: intl.formatMessage(messages.muteHashtag, {
hashtag,
}),
href: '/filters',
dangerous: true,
},
],
[intl, hashtag, account],
);
if (!open) {
return null;
}
return (
<Overlay
show={open}
offset={offset}
placement='bottom'
flip
target={targetRef}
popperConfig={popperConfig}
>
{({ props, arrowProps, placement }) => (
<div {...props}>
<div className={`dropdown-animation dropdown-menu ${placement}`}>
<div
className={`dropdown-menu__arrow ${placement}`}
{...arrowProps}
/>
<DropdownMenu
items={menu}
onClose={handleClose}
openedViaKeyboard={false}
/>
</div>
</div>
)}
</Overlay>
);
};

View file

@ -31,6 +31,7 @@ import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding
import BundleColumnError from './components/bundle_column_error';
import Header from './components/header';
import { UploadArea } from './components/upload_area';
import { HashtagMenuController } from './components/hashtag_menu_controller';
import ColumnsAreaContainer from './containers/columns_area_container';
import LoadingBarContainer from './containers/loading_bar_container';
import ModalContainer from './containers/modal_container';
@ -73,6 +74,7 @@ import {
About,
PrivacyPolicy,
TermsOfService,
AccountFeatured,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
@ -236,6 +238,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
@ -609,6 +612,7 @@ class UI extends PureComponent {
{layout !== 'mobile' && <PictureInPicture />}
<AlertsController />
{!disableHoverCards && <HoverCardController />}
<HashtagMenuController />
<LoadingBarContainer className='loading-bar' />
<ModalContainer />
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />

View file

@ -66,6 +66,10 @@ export function AccountGallery () {
return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery');
}
export function AccountFeatured() {
return import(/* webpackChunkName: "features/account_featured" */'../../account_featured');
}
export function Followers () {
return import(/* webpackChunkName: "features/followers" */'../../followers');
}

View file

@ -0,0 +1,37 @@
import { useEffect } from 'react';
import { useParams } from 'react-router';
import { fetchAccount, lookupAccount } from 'mastodon/actions/accounts';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
interface Params {
acct?: string;
id?: string;
}
export function useAccountId() {
const { acct, id } = useParams<Params>();
const accountId = useAppSelector(
(state) =>
id ??
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
);
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const isAccount = !!account;
const dispatch = useAppDispatch();
useEffect(() => {
if (!accountId) {
dispatch(lookupAccount(acct));
} else if (!isAccount) {
dispatch(fetchAccount(accountId));
}
}, [dispatch, accountId, acct, isAccount]);
return accountId;
}

View file

@ -0,0 +1,20 @@
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
export function useAccountVisibility(accountId?: string) {
const blockedBy = useAppSelector(
(state) => !!state.relationships.getIn([accountId, 'blocked_by'], false),
);
const suspended = useAppSelector(
(state) => !!state.accounts.getIn([accountId, 'suspended'], false),
);
const hidden = useAppSelector((state) =>
accountId ? Boolean(getAccountHidden(state, accountId)) : false,
);
return {
blockedBy,
suspended,
hidden,
};
}

View file

@ -18,7 +18,6 @@
"account.blocked": "Geblokkeer",
"account.cancel_follow_request": "Herroep volgversoek",
"account.disable_notifications": "Hou op om my van @{name} se plasings te laat weet",
"account.domain_blocked": "Domein geblokkeer",
"account.edit_profile": "Redigeer profiel",
"account.enable_notifications": "Laat weet my wanneer @{name} iets plaas",
"account.endorse": "Toon op profiel",

View file

@ -19,13 +19,11 @@
"account.blocked": "Blocau",
"account.cancel_follow_request": "Retirar solicitut de seguimiento",
"account.disable_notifications": "Deixar de notificar-me quan @{name} publique bella cosa",
"account.domain_blocked": "Dominio blocau",
"account.edit_profile": "Editar perfil",
"account.enable_notifications": "Notificar-me quan @{name} publique bella cosa",
"account.endorse": "Amostrar en perfil",
"account.featured_tags.last_status_at": "Zaguera publicación lo {date}",
"account.featured_tags.last_status_never": "Sin publicacions",
"account.featured_tags.title": "Etiquetas destacadas de {name}",
"account.follow": "Seguir",
"account.followers": "Seguidores",
"account.followers.empty": "Encara no sigue dengún a este usuario.",

View file

@ -23,13 +23,11 @@
"account.copy": "نسخ الرابط إلى الملف الشخصي",
"account.direct": "إشارة خاصة لـ @{name}",
"account.disable_notifications": "توقف عن إشعاري عندما ينشر @{name}",
"account.domain_blocked": "اسم النِّطاق محظور",
"account.edit_profile": "تعديل الملف الشخصي",
"account.enable_notifications": "أشعرني عندما ينشر @{name}",
"account.endorse": "أوصِ به على صفحتك الشخصية",
"account.featured_tags.last_status_at": "آخر منشور في {date}",
"account.featured_tags.last_status_never": "لا توجد رسائل",
"account.featured_tags.title": "وسوم {name} المميَّزة",
"account.follow": "متابعة",
"account.follow_back": "تابعه بالمثل",
"account.followers": "مُتابِعون",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "كتم الإشعارات",
"account.mute_short": "اكتم",
"account.muted": "مَكتوم",
"account.mutual": "متبادلة",
"account.no_bio": "لم يتم تقديم وصف.",
"account.open_original_page": "افتح الصفحة الأصلية",
"account.posts": "منشورات",

View file

@ -22,12 +22,10 @@
"account.copy": "Copiar l'enlllaz al perfil",
"account.direct": "Mentar a @{name} per privao",
"account.disable_notifications": "Dexar d'avisame cuando @{name} espublice artículos",
"account.domain_blocked": "Dominiu bloquiáu",
"account.edit_profile": "Editar el perfil",
"account.enable_notifications": "Avisame cuando @{name} espublice artículos",
"account.endorse": "Destacar nel perfil",
"account.featured_tags.last_status_never": "Nun hai nenguna publicación",
"account.featured_tags.title": "Etiquetes destacaes de: {name}",
"account.follow": "Siguir",
"account.follow_back": "Siguir tamién",
"account.followers": "Siguidores",

View file

@ -23,13 +23,11 @@
"account.copy": "Profil linkini kopyala",
"account.direct": "@{name} istifadəçisini fərdi olaraq etiketlə",
"account.disable_notifications": "@{name} paylaşım edəndə mənə bildiriş göndərməyi dayandır",
"account.domain_blocked": "Domen bloklanıb",
"account.edit_profile": "Profili redaktə et",
"account.enable_notifications": "@{name} paylaşım edəndə mənə bildiriş göndər",
"account.endorse": "Profildə seçilmişlərə əlavə et",
"account.featured_tags.last_status_at": "Son paylaşım {date} tarixində olub",
"account.featured_tags.last_status_never": "Paylaşım yoxdur",
"account.featured_tags.title": "{name} istifadəçisinin seçilmiş heşteqləri",
"account.follow": "İzlə",
"account.follow_back": "Sən də izlə",
"account.followers": "İzləyicilər",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "Bildirişləri səssizləşdir",
"account.mute_short": "Səssizləşdir",
"account.muted": "Səssizləşdirilib",
"account.mutual": "Ortaq",
"account.no_bio": "Təsvir göstərilməyib.",
"account.open_original_page": "Orijinal səhifəni aç",
"account.posts": "Paylaşım",

View file

@ -23,13 +23,11 @@
"account.copy": "Скапіраваць спасылку на профіль",
"account.direct": "Згадаць асабіста @{name}",
"account.disable_notifications": "Не паведамляць мне пра публікацыі @{name}",
"account.domain_blocked": "Дамен заблакаваны",
"account.edit_profile": "Рэдагаваць профіль",
"account.enable_notifications": "Апавяшчаць мяне пра допісы @{name}",
"account.endorse": "Паказваць у профілі",
"account.featured_tags.last_status_at": "Апошні допіс ад {date}",
"account.featured_tags.last_status_never": "Няма допісаў",
"account.featured_tags.title": "Тэгі, выбраныя {name}",
"account.follow": "Падпісацца",
"account.follow_back": "Падпісацца ў адказ",
"account.followers": "Падпісчыкі",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "Не апавяшчаць",
"account.mute_short": "Ігнараваць",
"account.muted": "Ігнаруецца",
"account.mutual": "Узаемныя",
"account.no_bio": "Апісанне адсутнічае.",
"account.open_original_page": "Адкрыць арыгінальную старонку",
"account.posts": "Допісы",

View file

@ -23,13 +23,14 @@
"account.copy": "Копиране на връзка към профила",
"account.direct": "Частно споменаване на @{name}",
"account.disable_notifications": "Спиране на известяване при публикуване от @{name}",
"account.domain_blocked": "Блокиран домейн",
"account.edit_profile": "Редактиране на профила",
"account.enable_notifications": "Известяване при публикуване от @{name}",
"account.endorse": "Представи в профила",
"account.featured": "Препоръчано",
"account.featured.hashtags": "Хаштагове",
"account.featured.posts": "Публикации",
"account.featured_tags.last_status_at": "Последна публикация на {date}",
"account.featured_tags.last_status_never": "Няма публикации",
"account.featured_tags.title": "Главни хаштагове на {name}",
"account.follow": "Последване",
"account.follow_back": "Последване взаимно",
"account.followers": "Последователи",
@ -52,7 +53,6 @@
"account.mute_notifications_short": "Заглушаване на известията",
"account.mute_short": "Заглушаване",
"account.muted": "Заглушено",
"account.mutual": "Взаимни",
"account.no_bio": "Няма представен опис.",
"account.open_original_page": "Отваряне на първообразната страница",
"account.posts": "Публикации",
@ -65,6 +65,7 @@
"account.statuses_counter": "{count, plural, one {{counter} публикация} other {{counter} публикации}}",
"account.unblock": "Отблокиране на @{name}",
"account.unblock_domain": "Отблокиране на домейн {domain}",
"account.unblock_domain_short": "Отблокиране",
"account.unblock_short": "Отблокиране",
"account.unendorse": "Не включвайте в профила",
"account.unfollow": "Стоп на следването",
@ -377,6 +378,8 @@
"generic.saved": "Запазено",
"getting_started.heading": "Първи стъпки",
"hashtag.admin_moderation": "Отваряне на модериращия интерфейс за #{name}",
"hashtag.browse": "Разглеждане на публикации в #{hashtag}",
"hashtag.browse_from_account": "Разглеждане на публикации от @{name} из #{hashtag}",
"hashtag.column_header.tag_mode.all": "и {additional}",
"hashtag.column_header.tag_mode.any": "или {additional}",
"hashtag.column_header.tag_mode.none": "без {additional}",
@ -390,6 +393,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} публикация} other {{counter} публикации}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} публикация} other {{counter} публикации}} днес",
"hashtag.follow": "Следване на хаштаг",
"hashtag.mute": "Заглушаване на #{hashtag}",
"hashtag.unfollow": "Спиране на следване на хаштаг",
"hashtags.and_other": "…и {count, plural, other {# още}}",
"hints.profiles.followers_may_be_missing": "Последователи за този профил може да липсват.",
@ -674,7 +678,7 @@
"onboarding.follows.title": "Последвайте хора, за да започнете",
"onboarding.profile.discoverable": "Правене на моя профил откриваем",
"onboarding.profile.discoverable_hint": "Включвайки откриваемостта в Mastodon, вашите публикации може да се появят при резултатите от търсене и изгряващи неща, и вашия профил може да бъде предложен на хора с подобни интереси като вашите.",
"onboarding.profile.display_name": "Името на показ",
"onboarding.profile.display_name": "Показвано име",
"onboarding.profile.display_name_hint": "Вашето пълно име или псевдоним…",
"onboarding.profile.note": "Биография",
"onboarding.profile.note_hint": "Може да @споменавате други хора или #хаштагове…",

View file

@ -23,13 +23,11 @@
"account.copy": "অবতারের সংযোগ অনুলিপি করো",
"account.direct": "গোপনে মেনশন করুন @{name}",
"account.disable_notifications": "আমাকে জানানো বন্ধ করো যখন @{name} পোস্ট করবে",
"account.domain_blocked": "ডোমেইন ব্লক করা",
"account.edit_profile": "প্রোফাইল সম্পাদনা করুন",
"account.enable_notifications": "আমাকে জানাবে যখন @{name} পোস্ট করবে",
"account.endorse": "প্রোফাইলে ফিচার করুন",
"account.featured_tags.last_status_at": "{date} এ সর্বশেষ পোস্ট",
"account.featured_tags.last_status_never": "কোনো পোস্ট নেই",
"account.featured_tags.title": "{name} এর ফিচার করা Hashtag সমূহ",
"account.follow": "অনুসরণ",
"account.follow_back": "তাকে অনুসরণ করো",
"account.followers": "অনুসরণকারী",

View file

@ -22,13 +22,11 @@
"account.copy": "Eilañ al liamm war-zu ho profil",
"account.direct": "Menegiñ @{name} ent-prevez",
"account.disable_notifications": "Paouez d'am c'hemenn pa vez embannet traoù gant @{name}",
"account.domain_blocked": "Domani stanket",
"account.edit_profile": "Kemmañ ar profil",
"account.enable_notifications": "Ma c'hemenn pa vez embannet traoù gant @{name}",
"account.endorse": "Lakaat war-wel war ar profil",
"account.featured_tags.last_status_at": "Toud diwezhañ : {date}",
"account.featured_tags.last_status_never": "Embannadur ebet",
"account.featured_tags.title": "Hashtagoù pennañ {name}",
"account.follow": "Heuliañ",
"account.follow_back": "Heuliañ d'ho tro",
"account.followers": "Tud koumanantet",

View file

@ -23,13 +23,14 @@
"account.copy": "Copia l'enllaç al perfil",
"account.direct": "Menciona privadament @{name}",
"account.disable_notifications": "Deixa de notificar-me els tuts de @{name}",
"account.domain_blocked": "Domini blocat",
"account.edit_profile": "Edita el perfil",
"account.enable_notifications": "Notifica'm els tuts de @{name}",
"account.endorse": "Recomana en el perfil",
"account.featured": "Destacat",
"account.featured.hashtags": "Etiquetes",
"account.featured.posts": "Publicacions",
"account.featured_tags.last_status_at": "Darrer tut el {date}",
"account.featured_tags.last_status_never": "No hi ha tuts",
"account.featured_tags.title": "etiquetes destacades de {name}",
"account.follow": "Segueix",
"account.follow_back": "Segueix tu també",
"account.followers": "Seguidors",
@ -52,7 +53,6 @@
"account.mute_notifications_short": "Silencia les notificacions",
"account.mute_short": "Silencia",
"account.muted": "Silenciat",
"account.mutual": "Mutu",
"account.no_bio": "No s'ha proporcionat cap descripció.",
"account.open_original_page": "Obre la pàgina original",
"account.posts": "Tuts",
@ -391,6 +391,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} tut} other {{counter} tuts}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} tut} other {{counter} tuts}} avui",
"hashtag.follow": "Segueix l'etiqueta",
"hashtag.mute": "Silencia #{hashtag}",
"hashtag.unfollow": "Deixa de seguir l'etiqueta",
"hashtags.and_other": "…i {count, plural, other {# més}}",
"hints.profiles.followers_may_be_missing": "Es poden haver perdut seguidors d'aquest perfil.",

View file

@ -22,13 +22,11 @@
"account.copy": "ڕوونووسی بەستەر بۆ توت",
"account.direct": "بە شێوەیەکی تایبەت باسی @{name} بکە",
"account.disable_notifications": "ئاگانامە مەنێرە بۆم کاتێک @{name} پۆست دەکرێت",
"account.domain_blocked": "دۆمەین قەپاتکرا",
"account.edit_profile": "دەستکاری پرۆفایل",
"account.enable_notifications": "ئاگادارم بکەوە کاتێک @{name} بابەتەکان",
"account.endorse": "ناساندن لە پرۆفایل",
"account.featured_tags.last_status_at": "دوایین پۆست لە {date}",
"account.featured_tags.last_status_never": "هیچ پۆستێک نییە",
"account.featured_tags.title": "هاشتاگە تایبەتەکانی {name}",
"account.follow": "بەدواداچوون",
"account.follow_back": "فۆڵۆو بکەنەوە",
"account.followers": "شوێنکەوتووان",
@ -49,7 +47,6 @@
"account.mute_notifications_short": "پاڵ بە ئاگادارکردنەوەکانەوە بنێ",
"account.mute_short": "بێدەنگ",
"account.muted": "بێ دەنگ",
"account.mutual": "دوولایەنە",
"account.no_bio": "هیچ وەسفێک نەخراوەتەڕوو.",
"account.open_original_page": "لاپەڕەی ئەسڵی بکەرەوە",
"account.posts": "نووسراوەکان",

View file

@ -7,7 +7,6 @@
"account.blocked": "Bluccatu",
"account.cancel_follow_request": "Withdraw follow request",
"account.disable_notifications": "Ùn mi nutificate più quandu @{name} pubblica qualcosa",
"account.domain_blocked": "Duminiu piattatu",
"account.edit_profile": "Mudificà u prufile",
"account.enable_notifications": "Nutificate mi quandu @{name} pubblica qualcosa",
"account.endorse": "Fà figurà nant'à u prufilu",

View file

@ -23,13 +23,14 @@
"account.copy": "Kopírovat odkaz na profil",
"account.direct": "Soukromě zmínit @{name}",
"account.disable_notifications": "Přestat mě upozorňovat, když @{name} zveřejní příspěvek",
"account.domain_blocked": "Doména blokována",
"account.edit_profile": "Upravit profil",
"account.enable_notifications": "Oznamovat mi příspěvky @{name}",
"account.endorse": "Zvýraznit na profilu",
"account.featured": "Doporučené",
"account.featured.hashtags": "Hashtagy",
"account.featured.posts": "Příspěvky",
"account.featured_tags.last_status_at": "Poslední příspěvek {date}",
"account.featured_tags.last_status_never": "Žádné příspěvky",
"account.featured_tags.title": "Hlavní hashtagy uživatele {name}",
"account.follow": "Sledovat",
"account.follow_back": "Také sledovat",
"account.followers": "Sledující",
@ -52,7 +53,6 @@
"account.mute_notifications_short": "Ztlumit upozornění",
"account.mute_short": "Ztlumit",
"account.muted": "Skrytý",
"account.mutual": "Vzájemné",
"account.no_bio": "Nebyl poskytnut žádný popis.",
"account.open_original_page": "Otevřít původní stránku",
"account.posts": "Příspěvky",
@ -378,6 +378,8 @@
"generic.saved": "Uloženo",
"getting_started.heading": "Začínáme",
"hashtag.admin_moderation": "Otevřít moderátorské rozhraní pro #{name}",
"hashtag.browse": "Procházet příspěvky na #{hashtag}",
"hashtag.browse_from_account": "Procházet příspěvky od @{name} v #{hashtag}",
"hashtag.column_header.tag_mode.all": "a {additional}",
"hashtag.column_header.tag_mode.any": "nebo {additional}",
"hashtag.column_header.tag_mode.none": "bez {additional}",
@ -391,6 +393,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} other {{counter} příspěvků}}",
"hashtag.counter_by_uses_today": "Dnes {count, plural, one {{counter} příspěvek} few {{counter} příspěvky} other {{counter} příspěvků}}",
"hashtag.follow": "Sledovat hashtag",
"hashtag.mute": "Skrýt #{hashtag}",
"hashtag.unfollow": "Přestat sledovat hashtag",
"hashtags.and_other": "…a {count, plural, one {# další} few {# další} other {# dalších}}",
"hints.profiles.followers_may_be_missing": "Sledující mohou pro tento profil chybět.",
@ -560,7 +563,7 @@
"notification.admin.sign_up.name_and_others": "{name} a {count, plural, one {# další} few {# další} many {# dalších} other {# dalších}} se zaregistrovali",
"notification.annual_report.message": "Váš #Wrapstodon {year} na Vás čeká! Podívejte se, jak vypadal tento Váš rok na Mastodonu!",
"notification.annual_report.view": "Zobrazit #Wrapstodon",
"notification.favourite": "{name} si oblíbil*a váš příspěvek",
"notification.favourite": "{name} si oblíbil váš příspěvek",
"notification.favourite.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Váš příspěvek",
"notification.favourite_pm": "{name} si oblíbil vaši soukromou zmínku",
"notification.favourite_pm.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Vaši soukromou zmínku",
@ -573,7 +576,7 @@
"notification.label.private_reply": "Privátní odpověď",
"notification.label.reply": "Odpověď",
"notification.mention": "Zmínka",
"notification.mentioned_you": "{name} vás zmínil*a",
"notification.mentioned_you": "{name} vás zmínil",
"notification.moderation-warning.learn_more": "Zjistit více",
"notification.moderation_warning": "Obdrželi jste varování od moderátorů",
"notification.moderation_warning.action_delete_statuses": "Některé z vašich příspěvků byly odstraněny.",

View file

@ -1,5 +1,5 @@
{
"about.blocks": "Gweinyddion wedi'u cymedroli",
"about.blocks": "Gweinyddion a gyfyngir",
"about.contact": "Cysylltwch â:",
"about.disclaimer": "Mae Mastodon yn feddalwedd cod agored rhydd ac o dan hawlfraint Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Nid yw'r rheswm ar gael",
@ -23,13 +23,14 @@
"account.copy": "Copïo dolen i'r proffil",
"account.direct": "Crybwyll yn breifat @{name}",
"account.disable_notifications": "Stopiwch fy hysbysu pan fydd @{name} yn postio",
"account.domain_blocked": "Parth wedi'i rwystro",
"account.edit_profile": "Golygu proffil",
"account.edit_profile": "Golygu'r proffil",
"account.enable_notifications": "Rhowch wybod i fi pan fydd @{name} yn postio",
"account.endorse": "Dangos ar fy mhroffil",
"account.featured": "Dethol",
"account.featured.hashtags": "Hashnodau",
"account.featured.posts": "Postiadau",
"account.featured_tags.last_status_at": "Y postiad olaf ar {date}",
"account.featured_tags.last_status_never": "Dim postiadau",
"account.featured_tags.title": "Prif hashnodau {name}",
"account.follow": "Dilyn",
"account.follow_back": "Dilyn nôl",
"account.followers": "Dilynwyr",
@ -52,7 +53,6 @@
"account.mute_notifications_short": "Diffodd hysbysiadau",
"account.mute_short": "Anwybyddu",
"account.muted": "Wedi anwybyddu",
"account.mutual": "Cydgydnabod",
"account.no_bio": "Dim disgrifiad wedi'i gynnig.",
"account.open_original_page": "Agor y dudalen wreiddiol",
"account.posts": "Postiadau",
@ -105,7 +105,7 @@
"annual_report.summary.here_it_is": "Dyma eich {year} yn gryno:",
"annual_report.summary.highlighted_post.by_favourites": "postiad wedi'i ffefrynu fwyaf",
"annual_report.summary.highlighted_post.by_reblogs": "postiad wedi'i hybu fwyaf",
"annual_report.summary.highlighted_post.by_replies": "postiad gyda'r nifer fwyaf o atebion",
"annual_report.summary.highlighted_post.by_replies": "postiad gyda'r nifer fwyaf o ymatebion",
"annual_report.summary.highlighted_post.possessive": "{name}",
"annual_report.summary.most_used_app.most_used_app": "ap a ddefnyddiwyd fwyaf",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "hashnod a ddefnyddiwyd fwyaf",
@ -226,7 +226,7 @@
"confirmations.mute.confirm": "Tewi",
"confirmations.redraft.confirm": "Dileu ac ailddrafftio",
"confirmations.redraft.message": "Ydych chi wir eisiau'r dileu'r postiad hwn a'i ailddrafftio? Bydd ffefrynnau a hybiau'n cael eu colli, a bydd atebion i'r post gwreiddiol yn mynd yn amddifad.",
"confirmations.redraft.title": "Dileu & ailddraftio postiad?",
"confirmations.redraft.title": "Dileu ac ailddraftio'r postiad?",
"confirmations.reply.confirm": "Ymateb",
"confirmations.reply.message": "Bydd ateb nawr yn cymryd lle y neges yr ydych yn cyfansoddi ar hyn o bryd. Ydych chi'n siŵr eich bod am barhau?",
"confirmations.reply.title": "Trosysgrifo'r postiad?",
@ -256,7 +256,7 @@
"dismissable_banner.explore_tags": "Mae'r hashnodau hyn ar gynnydd y ffedysawd heddiw. Mae hashnodau sy'n cael eu defnyddio gan fwy o bobl amrywiol yn cael eu graddio'n uwch.",
"dismissable_banner.public_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl ar y ffedysawd y mae pobl ar {domain} yn eu dilyn.",
"domain_block_modal.block": "Blocio gweinydd",
"domain_block_modal.block_account_instead": "Blocio @{name} yn ei le",
"domain_block_modal.block_account_instead": "Rhwystro @{name} yn lle hynny",
"domain_block_modal.they_can_interact_with_old_posts": "Gall pobl o'r gweinydd hwn ryngweithio â'ch hen bostiadau.",
"domain_block_modal.they_cant_follow": "All neb o'r gweinydd hwn eich dilyn.",
"domain_block_modal.they_wont_know": "Fyddan nhw ddim yn gwybod eu bod wedi cael eu blocio.",
@ -378,6 +378,8 @@
"generic.saved": "Wedi'i Gadw",
"getting_started.heading": "Dechrau",
"hashtag.admin_moderation": "Agor rhyngwyneb cymedroli #{name}",
"hashtag.browse": "Pori postiadau yn #{hashtag}",
"hashtag.browse_from_account": "Pori postiadau gan @{name} yn #{hashtag}",
"hashtag.column_header.tag_mode.all": "a {additional}",
"hashtag.column_header.tag_mode.any": "neu {additional}",
"hashtag.column_header.tag_mode.none": "heb {additional}",
@ -391,6 +393,7 @@
"hashtag.counter_by_uses": "{count, plural, one {postiad {counter}} other {postiad {counter}}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} postiad} other {{counter} postiad}} heddiw",
"hashtag.follow": "Dilyn hashnod",
"hashtag.mute": "Tewi #{hashtag}",
"hashtag.unfollow": "Dad-ddilyn hashnod",
"hashtags.and_other": "…a {count, plural, other {# arall}}",
"hints.profiles.followers_may_be_missing": "Mae'n bosibl bod dilynwyr y proffil hwn ar goll.",
@ -526,7 +529,7 @@
"navigation_bar.about": "Ynghylch",
"navigation_bar.administration": "Gweinyddiaeth",
"navigation_bar.advanced_interface": "Agor mewn rhyngwyneb gwe uwch",
"navigation_bar.blocks": "Defnyddwyr wedi eu blocio",
"navigation_bar.blocks": "Defnyddwyr wedi'u rhwystro",
"navigation_bar.bookmarks": "Nodau Tudalen",
"navigation_bar.community_timeline": "Ffrwd leol",
"navigation_bar.compose": "Cyfansoddi post newydd",
@ -537,7 +540,7 @@
"navigation_bar.favourites": "Ffefrynnau",
"navigation_bar.filters": "Geiriau wedi'u tewi",
"navigation_bar.follow_requests": "Ceisiadau dilyn",
"navigation_bar.followed_tags": "Hashnodau'n cael eu dilyn",
"navigation_bar.followed_tags": "Hashnodau a ddilynir",
"navigation_bar.follows_and_followers": "Yn dilyn a dilynwyr",
"navigation_bar.lists": "Rhestrau",
"navigation_bar.logout": "Allgofnodi",
@ -582,7 +585,7 @@
"notification.moderation_warning.action_none": "Mae eich cyfrif wedi derbyn rhybudd cymedroli.",
"notification.moderation_warning.action_sensitive": "Bydd eich postiadau'n cael eu marcio'n sensitif o hyn ymlaen.",
"notification.moderation_warning.action_silence": "Mae eich cyfrif wedi'i gyfyngu.",
"notification.moderation_warning.action_suspend": "Mae eich cyfrif wedi'i hatal.",
"notification.moderation_warning.action_suspend": "Mae eich cyfrif wedi'i atal.",
"notification.own_poll": "Mae eich pleidlais wedi dod i ben",
"notification.poll": "Mae arolwg y gwnaethoch bleidleisio ynddo wedi dod i ben",
"notification.reblog": "Hybodd {name} eich post",
@ -853,9 +856,9 @@
"status.remove_bookmark": "Tynnu nod tudalen",
"status.remove_favourite": "Tynnu o'r ffefrynnau",
"status.replied_in_thread": "Atebodd mewn edefyn",
"status.replied_to": "Wedi ateb {name}",
"status.replied_to": "Wedi ymateb i {name}",
"status.reply": "Ymateb",
"status.replyAll": "Ateb i edefyn",
"status.replyAll": "Ymateb i edefyn",
"status.report": "Adrodd ar @{name}",
"status.sensitive_warning": "Cynnwys sensitif",
"status.share": "Rhannu",

View file

@ -19,17 +19,20 @@
"account.block_domain": "Blokér domænet {domain}",
"account.block_short": "Bloker",
"account.blocked": "Blokeret",
"account.blocking": "Blokering",
"account.cancel_follow_request": "Annullér anmodning om at følge",
"account.copy": "Kopiér link til profil",
"account.direct": "Privat omtale @{name}",
"account.disable_notifications": "Advisér mig ikke længere, når @{name} poster",
"account.domain_blocked": "Domæne blokeret",
"account.domain_blocking": "Blokerer domæne",
"account.edit_profile": "Redigér profil",
"account.enable_notifications": "Advisér mig, når @{name} poster",
"account.endorse": "Fremhæv på profil",
"account.featured": "Fremhævet",
"account.featured.hashtags": "Hashtags",
"account.featured.posts": "Indlæg",
"account.featured_tags.last_status_at": "Seneste indlæg {date}",
"account.featured_tags.last_status_never": "Ingen indlæg",
"account.featured_tags.title": "{name}s fremhævede etiketter",
"account.follow": "Følg",
"account.follow_back": "Følg tilbage",
"account.followers": "Følgere",
@ -38,6 +41,7 @@
"account.following": "Følger",
"account.following_counter": "{count, plural, one {{counter} følger} other {{counter} følger}}",
"account.follows.empty": "Denne bruger følger ikke nogen endnu.",
"account.follows_you": "Følger dig",
"account.go_to_profile": "Gå til profil",
"account.hide_reblogs": "Skjul fremhævelser fra @{name}",
"account.in_memoriam": "Til minde om.",
@ -52,7 +56,8 @@
"account.mute_notifications_short": "Sluk for notifikationer",
"account.mute_short": "Skjul",
"account.muted": "Skjult",
"account.mutual": "Fælles",
"account.muting": "Tavsgørelse",
"account.mutual": "I følger hinanden",
"account.no_bio": "Ingen beskrivelse til rådighed.",
"account.open_original_page": "Åbn oprindelig side",
"account.posts": "Indlæg",
@ -60,6 +65,7 @@
"account.report": "Anmeld @{name}",
"account.requested": "Afventer godkendelse. Tryk for at annullere følgeanmodning",
"account.requested_follow": "{name} har anmodet om at følge dig",
"account.requests_to_follow_you": "Anmodninger om at følge dig",
"account.share": "Del @{name}s profil",
"account.show_reblogs": "Vis fremhævelser fra @{name}",
"account.statuses_counter": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}",
@ -294,6 +300,9 @@
"emoji_button.search_results": "Søgeresultater",
"emoji_button.symbols": "Symboler",
"emoji_button.travel": "Rejser og steder",
"empty_column.account_featured.me": "Du har ikke vist noget endnu. Vidste du, at man kan fremhæve sine indlæg, mest brugte hashtags og endda en vens konti på sin profil?",
"empty_column.account_featured.other": "{acct} har ikke vist noget endnu. Vidste du, at man kan fremhæve sine indlæg, mest brugte hashtags og endda en vens konti på sin profil?",
"empty_column.account_featured_other.unknown": "Denne konto har ikke fremhævet noget endnu.",
"empty_column.account_hides_collections": "Brugeren har valgt ikke at gøre denne information tilgængelig",
"empty_column.account_suspended": "Konto suspenderet",
"empty_column.account_timeline": "Ingen indlæg her!",
@ -378,6 +387,8 @@
"generic.saved": "Gemt",
"getting_started.heading": "Startmenu",
"hashtag.admin_moderation": "Åbn modereringsbrugerflade for #{name}",
"hashtag.browse": "Gennemse indlæg i #{hashtag}",
"hashtag.browse_from_account": "Gennemse indlæg fra @{name} i #{hashtag}",
"hashtag.column_header.tag_mode.all": "og {additional}",
"hashtag.column_header.tag_mode.any": "eller {additional}",
"hashtag.column_header.tag_mode.none": "uden {additional}",
@ -391,6 +402,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}} i dag",
"hashtag.follow": "Følg etiket",
"hashtag.mute": "Tavsgør #{hashtag}",
"hashtag.unfollow": "Stop med at følge etiket",
"hashtags.and_other": "…og {count, plural, one {}other {# flere}}",
"hints.profiles.followers_may_be_missing": "Der kan mangle følgere for denne profil.",

View file

@ -19,17 +19,20 @@
"account.block_domain": "{domain} sperren",
"account.block_short": "Blockieren",
"account.blocked": "Blockiert",
"account.blocking": "Gesperrt",
"account.cancel_follow_request": "Follower-Anfrage zurückziehen",
"account.copy": "Link zum Profil kopieren",
"account.direct": "@{name} privat erwähnen",
"account.disable_notifications": "Höre auf mich zu benachrichtigen wenn @{name} etwas postet",
"account.domain_blocked": "Domain versteckt",
"account.domain_blocking": "Domain gesperrt",
"account.edit_profile": "Profil bearbeiten",
"account.enable_notifications": "Benachrichtige mich wenn @{name} etwas postet",
"account.endorse": "Im Profil empfehlen",
"account.featured": "Vorgestellt",
"account.featured.hashtags": "Hashtags",
"account.featured.posts": "Beiträge",
"account.featured_tags.last_status_at": "Letzter Beitrag am {date}",
"account.featured_tags.last_status_never": "Keine Beiträge",
"account.featured_tags.title": "Von {name} vorgestellte Hashtags",
"account.follow": "Folgen",
"account.follow_back": "Ebenfalls folgen",
"account.followers": "Follower",
@ -38,6 +41,7 @@
"account.following": "Folge ich",
"account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}",
"account.follows.empty": "Dieses Profil folgt noch niemandem.",
"account.follows_you": "Folgt dir",
"account.go_to_profile": "Profil aufrufen",
"account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden",
"account.in_memoriam": "Zum Andenken.",
@ -52,7 +56,8 @@
"account.mute_notifications_short": "Benachrichtigungen stummschalten",
"account.mute_short": "Stummschalten",
"account.muted": "Stummgeschaltet",
"account.mutual": "Gegenseitig",
"account.muting": "Wird stummgeschaltet",
"account.mutual": "Ihr folgt euch",
"account.no_bio": "Keine Beschreibung verfügbar.",
"account.open_original_page": "Ursprüngliche Seite öffnen",
"account.posts": "Beiträge",
@ -60,6 +65,7 @@
"account.report": "@{name} melden",
"account.requested": "Die Genehmigung steht noch aus. Klicke hier, um die Follower-Anfrage zurückzuziehen",
"account.requested_follow": "{name} möchte dir folgen",
"account.requests_to_follow_you": "Möchte dir folgen",
"account.share": "Profil von @{name} teilen",
"account.show_reblogs": "Geteilte Beiträge von @{name} anzeigen",
"account.statuses_counter": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
@ -294,6 +300,9 @@
"emoji_button.search_results": "Suchergebnisse",
"emoji_button.symbols": "Symbole",
"emoji_button.travel": "Reisen & Orte",
"empty_column.account_featured.me": "Du hast bisher noch nichts vorgestellt. Wusstest du, dass du deine Beiträge, häufig verwendete Hashtags und sogar andere Profile vorstellen kannst?",
"empty_column.account_featured.other": "{acct} hat bisher noch nichts vorgestellt. Wusstest du, dass du deine Beiträge, häufig verwendete Hashtags und sogar andere Profile vorstellen kannst?",
"empty_column.account_featured_other.unknown": "Dieses Profil hat bisher noch nichts vorgestellt.",
"empty_column.account_hides_collections": "Das Konto hat sich dazu entschieden, diese Information nicht zu veröffentlichen",
"empty_column.account_suspended": "Konto gesperrt",
"empty_column.account_timeline": "Keine Beiträge vorhanden!",
@ -378,6 +387,8 @@
"generic.saved": "Gespeichert",
"getting_started.heading": "Auf gehts!",
"hashtag.admin_moderation": "#{name} moderieren",
"hashtag.browse": "Beiträge mit #{hashtag} suchen",
"hashtag.browse_from_account": "Beiträge von @{name} mit #{hashtag} suchen",
"hashtag.column_header.tag_mode.all": "und {additional}",
"hashtag.column_header.tag_mode.any": "oder {additional}",
"hashtag.column_header.tag_mode.none": "ohne {additional}",
@ -391,6 +402,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}} heute",
"hashtag.follow": "Hashtag folgen",
"hashtag.mute": "#{hashtag} stummschalten",
"hashtag.unfollow": "Hashtag entfolgen",
"hashtags.and_other": "… und {count, plural, one{# weiterer} other {# weitere}}",
"hints.profiles.followers_may_be_missing": "Möglicherweise werden für dieses Profil nicht alle Follower angezeigt.",

View file

@ -23,13 +23,14 @@
"account.copy": "Αντιγραφή συνδέσμου προφίλ",
"account.direct": "Ιδιωτική αναφορά @{name}",
"account.disable_notifications": "Σταμάτα να με ειδοποιείς όταν δημοσιεύει ο @{name}",
"account.domain_blocked": "Ο τομέας αποκλείστηκε",
"account.edit_profile": "Επεξεργασία προφίλ",
"account.enable_notifications": "Ειδοποίησέ με όταν δημοσιεύει ο @{name}",
"account.endorse": "Προβολή στο προφίλ",
"account.featured": "Προτεινόμενα",
"account.featured.hashtags": "Ετικέτες",
"account.featured.posts": "Αναρτήσεις",
"account.featured_tags.last_status_at": "Τελευταία ανάρτηση στις {date}",
"account.featured_tags.last_status_never": "Καμία ανάρτηση",
"account.featured_tags.title": "προβεβλημένες ετικέτες του/της {name}",
"account.follow": "Ακολούθησε",
"account.follow_back": "Ακολούθησε και εσύ",
"account.followers": "Ακόλουθοι",
@ -52,7 +53,6 @@
"account.mute_notifications_short": "Σίγαση ειδοποιήσεων",
"account.mute_short": "Σίγαση",
"account.muted": "Αποσιωπημένος/η",
"account.mutual": "Αμοιβαίοι",
"account.no_bio": "Δεν υπάρχει περιγραφή.",
"account.open_original_page": "Ανοικτό",
"account.posts": "Τουτ",
@ -65,6 +65,7 @@
"account.statuses_counter": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
"account.unblock": "Άρση αποκλεισμού @{name}",
"account.unblock_domain": "Άρση αποκλεισμού του τομέα {domain}",
"account.unblock_domain_short": "Άρση αποκλ.",
"account.unblock_short": "Άρση αποκλεισμού",
"account.unendorse": "Να μην παρέχεται στο προφίλ",
"account.unfollow": "Άρση ακολούθησης",
@ -377,6 +378,8 @@
"generic.saved": "Αποθηκεύτηκε",
"getting_started.heading": "Ας ξεκινήσουμε",
"hashtag.admin_moderation": "Άνοιγμα διεπαφής συντονισμού για το #{name}",
"hashtag.browse": "Περιήγηση αναρτήσεων με #{hashtag}",
"hashtag.browse_from_account": "Περιήγηση αναρτήσεων από @{name} σε #{hashtag}",
"hashtag.column_header.tag_mode.all": "και {additional}",
"hashtag.column_header.tag_mode.any": "ή {additional}",
"hashtag.column_header.tag_mode.none": "χωρίς {additional}",
@ -390,6 +393,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}} σήμερα",
"hashtag.follow": "Παρακολούθηση ετικέτας",
"hashtag.mute": "Σίγαση #{hashtag}",
"hashtag.unfollow": "Διακοπή παρακολούθησης ετικέτας",
"hashtags.and_other": "…και {count, plural, one {}other {# ακόμη}}",
"hints.profiles.followers_may_be_missing": "Μπορεί να λείπουν ακόλουθοι για αυτό το προφίλ.",
@ -872,7 +876,9 @@
"subscribed_languages.target": "Αλλαγή εγγεγραμμένων γλωσσών για {target}",
"tabs_bar.home": "Αρχική",
"tabs_bar.notifications": "Ειδοποιήσεις",
"terms_of_service.effective_as_of": "Ενεργό από {date}",
"terms_of_service.title": "Όροι Παροχής Υπηρεσιών",
"terms_of_service.upcoming_changes_on": "Επερχόμενες αλλαγές στις {date}",
"time_remaining.days": "απομένουν {number, plural, one {# ημέρα} other {# ημέρες}}",
"time_remaining.hours": "απομένουν {number, plural, one {# ώρα} other {# ώρες}}",
"time_remaining.minutes": "απομένουν {number, plural, one {# λεπτό} other {# λεπτά}}",
@ -903,6 +909,12 @@
"video.expand": "Επέκταση βίντεο",
"video.fullscreen": "Πλήρης οθόνη",
"video.hide": "Απόκρυψη βίντεο",
"video.mute": "Σίγαση",
"video.pause": "Παύση",
"video.play": "Αναπαραγωγή"
"video.play": "Αναπαραγωγή",
"video.skip_backward": "Παράλειψη πίσω",
"video.skip_forward": "Παράλειψη εμπρός",
"video.unmute": "Άρση σίγασης",
"video.volume_down": "Μείωση έντασης",
"video.volume_up": "Αύξηση έντασης"
}

View file

@ -23,13 +23,11 @@
"account.copy": "Copy link to profile",
"account.direct": "Privately mention @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Domain blocked",
"account.edit_profile": "Edit profile",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Feature on profile",
"account.featured_tags.last_status_at": "Last post on {date}",
"account.featured_tags.last_status_never": "No posts",
"account.featured_tags.title": "{name}'s featured hashtags",
"account.follow": "Follow",
"account.follow_back": "Follow back",
"account.followers": "Followers",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "Mute notifications",
"account.mute_short": "Mute",
"account.muted": "Muted",
"account.mutual": "Mutual",
"account.no_bio": "No description provided.",
"account.open_original_page": "Open original page",
"account.posts": "Posts",

View file

@ -19,17 +19,20 @@
"account.block_domain": "Block domain {domain}",
"account.block_short": "Block",
"account.blocked": "Blocked",
"account.blocking": "Blocking",
"account.cancel_follow_request": "Cancel follow",
"account.copy": "Copy link to profile",
"account.direct": "Privately mention @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Domain blocked",
"account.domain_blocking": "Blocking domain",
"account.edit_profile": "Edit profile",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Feature on profile",
"account.featured": "Featured",
"account.featured.hashtags": "Hashtags",
"account.featured.posts": "Posts",
"account.featured_tags.last_status_at": "Last post on {date}",
"account.featured_tags.last_status_never": "No posts",
"account.featured_tags.title": "{name}'s featured hashtags",
"account.follow": "Follow",
"account.follow_back": "Follow back",
"account.followers": "Followers",
@ -38,6 +41,7 @@
"account.following": "Following",
"account.following_counter": "{count, plural, one {{counter} following} other {{counter} following}}",
"account.follows.empty": "This user doesn't follow anyone yet.",
"account.follows_you": "Follows you",
"account.go_to_profile": "Go to profile",
"account.hide_reblogs": "Hide boosts from @{name}",
"account.in_memoriam": "In Memoriam.",
@ -52,14 +56,17 @@
"account.mute_notifications_short": "Mute notifications",
"account.mute_short": "Mute",
"account.muted": "Muted",
"account.mutual": "Mutual",
"account.muting": "Muting",
"account.mutual": "You follow each other",
"account.no_bio": "No description provided.",
"account.open_original_page": "Open original page",
"account.posts": "Posts",
"account.posts_with_replies": "Posts and replies",
"account.remove_from_followers": "Remove {name} from followers",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval. Click to cancel follow request",
"account.requested_follow": "{name} has requested to follow you",
"account.requests_to_follow_you": "Requests to follow you",
"account.share": "Share @{name}'s profile",
"account.show_reblogs": "Show boosts from @{name}",
"account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} posts}}",
@ -227,6 +234,9 @@
"confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.",
"confirmations.redraft.title": "Delete & redraft post?",
"confirmations.remove_from_followers.confirm": "Remove follower",
"confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?",
"confirmations.remove_from_followers.title": "Remove follower?",
"confirmations.reply.confirm": "Reply",
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.reply.title": "Overwrite post?",
@ -294,6 +304,9 @@
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.account_featured.me": "You have not featured anything yet. Did you know that you can feature your posts, hashtags you use the most, and even your friends accounts on your profile?",
"empty_column.account_featured.other": "{acct} has not featured anything yet. Did you know that you can feature your posts, hashtags you use the most, and even your friends accounts on your profile?",
"empty_column.account_featured_other.unknown": "This account has not featured anything yet.",
"empty_column.account_hides_collections": "This user has chosen to not make this information available",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "No posts here!",
@ -378,6 +391,8 @@
"generic.saved": "Saved",
"getting_started.heading": "Getting started",
"hashtag.admin_moderation": "Open moderation interface for #{name}",
"hashtag.browse": "Browse posts in #{hashtag}",
"hashtag.browse_from_account": "Browse posts from @{name} in #{hashtag}",
"hashtag.column_header.tag_mode.all": "and {additional}",
"hashtag.column_header.tag_mode.any": "or {additional}",
"hashtag.column_header.tag_mode.none": "without {additional}",
@ -391,6 +406,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} post} other {{counter} posts}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today",
"hashtag.follow": "Follow hashtag",
"hashtag.mute": "Mute #{hashtag}",
"hashtag.unfollow": "Unfollow hashtag",
"hashtags.and_other": "…and {count, plural, other {# more}}",
"hints.profiles.followers_may_be_missing": "Followers for this profile may be missing.",

View file

@ -23,13 +23,13 @@
"account.copy": "Kopii ligilon al profilo",
"account.direct": "Private mencii @{name}",
"account.disable_notifications": "Ĉesu sciigi min kiam @{name} afiŝas",
"account.domain_blocked": "Domajno blokita",
"account.edit_profile": "Redakti la profilon",
"account.enable_notifications": "Sciigu min kiam @{name} afiŝos",
"account.endorse": "Prezenti ĉe via profilo",
"account.featured.hashtags": "Kradvortoj",
"account.featured.posts": "Afiŝoj",
"account.featured_tags.last_status_at": "Lasta afîŝo je {date}",
"account.featured_tags.last_status_never": "Neniu afiŝo",
"account.featured_tags.title": "Rekomendataj kradvortoj de {name}",
"account.follow": "Sekvi",
"account.follow_back": "Sekvu reen",
"account.followers": "Sekvantoj",
@ -52,7 +52,6 @@
"account.mute_notifications_short": "Silentigu sciigojn",
"account.mute_short": "Silentigu",
"account.muted": "Silentigita",
"account.mutual": "Reciproka",
"account.no_bio": "Neniu priskribo estas provizita.",
"account.open_original_page": "Malfermi la originalan paĝon",
"account.posts": "Afiŝoj",
@ -65,6 +64,7 @@
"account.statuses_counter": "{count, plural,one {{counter} afiŝo} other {{counter} afiŝoj}}",
"account.unblock": "Malbloki @{name}",
"account.unblock_domain": "Malbloki la domajnon {domain}",
"account.unblock_domain_short": "Malbloki",
"account.unblock_short": "Malbloki",
"account.unendorse": "Ne plu rekomendi ĉe la profilo",
"account.unfollow": "Ĉesi sekvi",
@ -905,6 +905,8 @@
"video.expand": "Pligrandigi la videon",
"video.fullscreen": "Igi plenekrana",
"video.hide": "Kaŝu la filmeton",
"video.mute": "Silentigi",
"video.pause": "Paŭzigi",
"video.play": "Ekigi"
"video.play": "Ekigi",
"video.unmute": "Ne plu silentigi"
}

View file

@ -19,17 +19,20 @@
"account.block_domain": "Bloquear dominio {domain}",
"account.block_short": "Bloquear",
"account.blocked": "Bloqueado",
"account.blocking": "Bloqueo",
"account.cancel_follow_request": "Dejar de seguir",
"account.copy": "Copiar enlace al perfil",
"account.direct": "Mención privada a @{name}",
"account.disable_notifications": "Dejar de notificarme cuando @{name} envíe mensajes",
"account.domain_blocked": "Dominio bloqueado",
"account.domain_blocking": "Bloqueando dominio",
"account.edit_profile": "Editar perfil",
"account.enable_notifications": "Notificarme cuando @{name} envíe mensajes",
"account.endorse": "Destacar en el perfil",
"account.featured": "Destacados",
"account.featured.hashtags": "Etiquetas",
"account.featured.posts": "Mensajes",
"account.featured_tags.last_status_at": "Último mensaje: {date}",
"account.featured_tags.last_status_never": "Sin mensajes",
"account.featured_tags.title": "Etiquetas destacadas de {name}",
"account.follow": "Seguir",
"account.follow_back": "Seguir",
"account.followers": "Seguidores",
@ -38,6 +41,7 @@
"account.following": "Siguiendo",
"account.following_counter": "{count, plural, one {siguiendo a {counter}} other {siguiendo a {counter}}}",
"account.follows.empty": "Todavía este usuario no sigue a nadie.",
"account.follows_you": "Te sigue",
"account.go_to_profile": "Ir al perfil",
"account.hide_reblogs": "Ocultar adhesiones de @{name}",
"account.in_memoriam": "Cuenta conmemorativa.",
@ -52,7 +56,8 @@
"account.mute_notifications_short": "Silenciar notificaciones",
"account.mute_short": "Silenciar",
"account.muted": "Silenciado",
"account.mutual": "Seguimiento mutuo",
"account.muting": "Silenciada",
"account.mutual": "Se siguen mutuamente",
"account.no_bio": "Sin descripción provista.",
"account.open_original_page": "Abrir página original",
"account.posts": "Mensajes",
@ -60,6 +65,7 @@
"account.report": "Denunciar a @{name}",
"account.requested": "Esperando aprobación. Hacé clic para cancelar la solicitud de seguimiento",
"account.requested_follow": "{name} solicitó seguirte",
"account.requests_to_follow_you": "Solicita seguirte",
"account.share": "Compartir el perfil de @{name}",
"account.show_reblogs": "Mostrar adhesiones de @{name}",
"account.statuses_counter": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
@ -294,6 +300,9 @@
"emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares",
"empty_column.account_featured.me": "Todavía no destacaste nada. ¿Sabías que en tu perfil podés destacar tus publicaciones, las etiquetas que más usás e incluso las cuentas de tus contactos?",
"empty_column.account_featured.other": "{acct} todavía no destacó nada. ¿Sabías que en tu perfil podés destacar tus publicaciones, las etiquetas que más usás e incluso las cuentas de tus contactos?",
"empty_column.account_featured_other.unknown": "Esta cuenta todavía no destacó nada.",
"empty_column.account_hides_collections": "Este usuario eligió no publicar esta información",
"empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡No hay mensajes acá!",
@ -378,6 +387,8 @@
"generic.saved": "Guardado",
"getting_started.heading": "Inicio de Mastodon",
"hashtag.admin_moderation": "Abrir interface de moderación para #{name}",
"hashtag.browse": "Ver publicaciones con #{hashtag}",
"hashtag.browse_from_account": "Ver publicaciones de @{name} con #{hashtag}",
"hashtag.column_header.tag_mode.all": "y {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "sin {additional}",
@ -391,6 +402,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}} hoy",
"hashtag.follow": "Seguir etiqueta",
"hashtag.mute": "Silenciar #{hashtag}",
"hashtag.unfollow": "Dejar de seguir etiqueta",
"hashtags.and_other": "…y {count, plural, other {# más}}",
"hints.profiles.followers_may_be_missing": "Es posible que falten seguidores de este perfil.",

View file

@ -19,17 +19,20 @@
"account.block_domain": "Bloquear dominio {domain}",
"account.block_short": "Bloquear",
"account.blocked": "Bloqueado",
"account.blocking": "Bloqueando",
"account.cancel_follow_request": "Cancelar seguimiento",
"account.copy": "Copiar enlace al perfil",
"account.direct": "Mención privada @{name}",
"account.disable_notifications": "Dejar de notificarme cuando @{name} publique algo",
"account.domain_blocked": "Dominio oculto",
"account.domain_blocking": "Bloqueando dominio",
"account.edit_profile": "Editar perfil",
"account.enable_notifications": "Notificarme cuando @{name} publique algo",
"account.endorse": "Destacar en mi perfil",
"account.featured": "Destacado",
"account.featured.hashtags": "Etiquetas",
"account.featured.posts": "Publicaciones",
"account.featured_tags.last_status_at": "Última publicación el {date}",
"account.featured_tags.last_status_never": "Sin publicaciones",
"account.featured_tags.title": "Etiquetas destacadas de {name}",
"account.follow": "Seguir",
"account.follow_back": "Seguir también",
"account.followers": "Seguidores",
@ -38,6 +41,7 @@
"account.following": "Siguiendo",
"account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}",
"account.follows.empty": "Este usuario no sigue a nadie todavía.",
"account.follows_you": "Te sigue",
"account.go_to_profile": "Ir al perfil",
"account.hide_reblogs": "Ocultar impulsos de @{name}",
"account.in_memoriam": "En memoria.",
@ -52,7 +56,8 @@
"account.mute_notifications_short": "Silenciar notificaciones",
"account.mute_short": "Silenciar",
"account.muted": "Silenciado",
"account.mutual": "Mutuo",
"account.muting": "Silenciando",
"account.mutual": "Se siguen el uno al otro",
"account.no_bio": "Sin biografía.",
"account.open_original_page": "Abrir página original",
"account.posts": "Publicaciones",
@ -60,6 +65,7 @@
"account.report": "Denunciar a @{name}",
"account.requested": "Esperando aprobación. Haz clic para cancelar la solicitud de seguimiento",
"account.requested_follow": "{name} ha solicitado seguirte",
"account.requests_to_follow_you": "Solicita seguirte",
"account.share": "Compartir el perfil de @{name}",
"account.show_reblogs": "Mostrar impulsos de @{name}",
"account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
@ -294,6 +300,9 @@
"emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares",
"empty_column.account_featured.me": "No has destacado nada todavía. ¿Sabías que puedes destacar tus publicaciones, las etiquetas que más usas e incluso las cuentas de tus amigos en tu perfil?",
"empty_column.account_featured.other": "{acct} no ha destacado nada todavía. ¿Sabías que puedes destacar tus publicaciones, las etiquetas que más usas e incluso las cuentas de tus amigos en tu perfil?",
"empty_column.account_featured_other.unknown": "Esta cuenta no ha destacado nada todavía.",
"empty_column.account_hides_collections": "Este usuario ha elegido no hacer disponible esta información",
"empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡No hay publicaciones aquí!",
@ -378,6 +387,8 @@
"generic.saved": "Guardado",
"getting_started.heading": "Primeros pasos",
"hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
"hashtag.browse": "Explorar publicaciones en #{hashtag}",
"hashtag.browse_from_account": "Explorar publicaciones de @{name} en #{hashtag}",
"hashtag.column_header.tag_mode.all": "y {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "sin {additional}",
@ -391,6 +402,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}} hoy",
"hashtag.follow": "Seguir etiqueta",
"hashtag.mute": "Silenciar #{hashtag}",
"hashtag.unfollow": "Dejar de seguir etiqueta",
"hashtags.and_other": "…y {count, plural, other {# más}}",
"hints.profiles.followers_may_be_missing": "Puede que no se muestren todos los seguidores de este perfil.",
@ -910,8 +922,8 @@
"video.pause": "Pausar",
"video.play": "Reproducir",
"video.skip_backward": "Saltar atrás",
"video.skip_forward": "Adelantar",
"video.skip_forward": "Saltar adelante",
"video.unmute": "Dejar de silenciar",
"video.volume_down": "Bajar volumen",
"video.volume_up": "Subir volumen"
"video.volume_down": "Bajar el volumen",
"video.volume_up": "Subir el volumen"
}

View file

@ -19,17 +19,20 @@
"account.block_domain": "Bloquear dominio {domain}",
"account.block_short": "Bloquear",
"account.blocked": "Bloqueado",
"account.blocking": "Bloqueando",
"account.cancel_follow_request": "Retirar solicitud de seguimiento",
"account.copy": "Copiar enlace al perfil",
"account.direct": "Mención privada a @{name}",
"account.disable_notifications": "Dejar de notificarme cuando @{name} publique algo",
"account.domain_blocked": "Dominio bloqueado",
"account.domain_blocking": "Bloqueando dominio",
"account.edit_profile": "Editar perfil",
"account.enable_notifications": "Notificarme cuando @{name} publique algo",
"account.endorse": "Destacar en el perfil",
"account.featured": "Destacado",
"account.featured.hashtags": "Etiquetas",
"account.featured.posts": "Publicaciones",
"account.featured_tags.last_status_at": "Última publicación el {date}",
"account.featured_tags.last_status_never": "Sin publicaciones",
"account.featured_tags.title": "Etiquetas destacadas de {name}",
"account.follow": "Seguir",
"account.follow_back": "Seguir también",
"account.followers": "Seguidores",
@ -38,6 +41,7 @@
"account.following": "Siguiendo",
"account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}",
"account.follows.empty": "Este usuario todavía no sigue a nadie.",
"account.follows_you": "Te sigue",
"account.go_to_profile": "Ir al perfil",
"account.hide_reblogs": "Ocultar impulsos de @{name}",
"account.in_memoriam": "Cuenta conmemorativa.",
@ -52,7 +56,8 @@
"account.mute_notifications_short": "Silenciar notificaciones",
"account.mute_short": "Silenciar",
"account.muted": "Silenciado",
"account.mutual": "Mutuo",
"account.muting": "Silenciando",
"account.mutual": "Os seguís mutuamente",
"account.no_bio": "Sin biografía.",
"account.open_original_page": "Abrir página original",
"account.posts": "Publicaciones",
@ -60,6 +65,7 @@
"account.report": "Reportar a @{name}",
"account.requested": "Esperando aprobación. Haz clic para cancelar la solicitud de seguimiento",
"account.requested_follow": "{name} ha solicitado seguirte",
"account.requests_to_follow_you": "Solicita seguirte",
"account.share": "Compartir el perfil de @{name}",
"account.show_reblogs": "Mostrar impulsos de @{name}",
"account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
@ -294,6 +300,9 @@
"emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares",
"empty_column.account_featured.me": "Aún no has destacado nada. ¿Sabías que puedes destacar tus publicaciones, las etiquetas que más utilizas e incluso las cuentas de tus amigos en tu perfil?",
"empty_column.account_featured.other": "{acct} no ha destacado nada aún. ¿Sabías que puedes destacar tus publicaciones, las etiquetas que más utilizas e incluso las cuentas de tus amigos en tu perfil?",
"empty_column.account_featured_other.unknown": "Esta cuenta aún no ha destacado nada.",
"empty_column.account_hides_collections": "Este usuario ha decidido no mostrar esta información",
"empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡No hay publicaciones aquí!",
@ -378,6 +387,8 @@
"generic.saved": "Guardado",
"getting_started.heading": "Primeros pasos",
"hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
"hashtag.browse": "Explorar publicaciones en #{hashtag}",
"hashtag.browse_from_account": "Explorar publicaciones de @{name} en #{hashtag}",
"hashtag.column_header.tag_mode.all": "y {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "sin {additional}",
@ -391,6 +402,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}} hoy",
"hashtag.follow": "Seguir etiqueta",
"hashtag.mute": "Silenciar #{hashtag}",
"hashtag.unfollow": "Dejar de seguir etiqueta",
"hashtags.and_other": "…y {count, plural, other {# más}}",
"hints.profiles.followers_may_be_missing": "Puede que no se muestren todos los seguidores de este perfil.",

View file

@ -23,13 +23,11 @@
"account.copy": "Kopeeri profiili link",
"account.direct": "Maini privaatselt @{name}",
"account.disable_notifications": "Peata teavitused @{name} postitustest",
"account.domain_blocked": "Domeen peidetud",
"account.edit_profile": "Muuda profiili",
"account.enable_notifications": "Teavita mind @{name} postitustest",
"account.endorse": "Too profiilil esile",
"account.featured_tags.last_status_at": "Viimane postitus {date}",
"account.featured_tags.last_status_never": "Postitusi pole",
"account.featured_tags.title": "{name} esiletõstetud sildid",
"account.follow": "Jälgi",
"account.follow_back": "Jälgi vastu",
"account.followers": "Jälgijad",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "Vaigista teavitused",
"account.mute_short": "Vaigista",
"account.muted": "Vaigistatud",
"account.mutual": "Jälgite",
"account.no_bio": "Kirjeldust pole lisatud.",
"account.open_original_page": "Ava algne leht",
"account.posts": "Postitused",

View file

@ -23,13 +23,11 @@
"account.copy": "Kopiatu profilerako esteka",
"account.direct": "Aipatu pribatuki @{name}",
"account.disable_notifications": "Utzi jakinarazteari @{name} erabiltzaileak argitaratzean",
"account.domain_blocked": "Ezkutatutako domeinua",
"account.edit_profile": "Editatu profila",
"account.enable_notifications": "Jakinarazi @{name} erabiltzaileak argitaratzean",
"account.endorse": "Nabarmendu profilean",
"account.featured_tags.last_status_at": "Azken bidalketa {date} datan",
"account.featured_tags.last_status_never": "Bidalketarik ez",
"account.featured_tags.title": "{name} erabiltzailearen nabarmendutako traolak",
"account.follow": "Jarraitu",
"account.follow_back": "Jarraitu bueltan",
"account.followers": "Jarraitzaileak",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "Mututu jakinarazpenak",
"account.mute_short": "Mututu",
"account.muted": "Mutututa",
"account.mutual": "Elkarrekikoa",
"account.no_bio": "Ez da deskribapenik eman.",
"account.open_original_page": "Ireki jatorrizko orria",
"account.posts": "Bidalketa",

View file

@ -23,13 +23,11 @@
"account.copy": "رونوشت از پیوند به نمایه",
"account.direct": "اشارهٔ خصوصی به @{name}",
"account.disable_notifications": "آگاه کردن من هنگام فرسته‌های @{name} را متوقّف کن",
"account.domain_blocked": "دامنه مسدود شد",
"account.edit_profile": "ویرایش نمایه",
"account.enable_notifications": "هنگام فرسته‌های @{name} مرا آگاه کن",
"account.endorse": "معرّفی در نمایه",
"account.featured_tags.last_status_at": "آخرین فرسته در {date}",
"account.featured_tags.last_status_never": "بدون فرسته",
"account.featured_tags.title": "برچسب‌های برگزیدهٔ {name}",
"account.follow": "پی‌گرفتن",
"account.follow_back": "دنبال کردن متقابل",
"account.followers": "پی‌گیرندگان",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "خموشی آگاهی‌ها",
"account.mute_short": "خموشی",
"account.muted": "خموش",
"account.mutual": "دوطرفه",
"account.no_bio": "شرحی فراهم نشده.",
"account.open_original_page": "گشودن صفحهٔ اصلی",
"account.posts": "فرسته",
@ -65,6 +62,7 @@
"account.statuses_counter": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}",
"account.unblock": "رفع مسدودیت @{name}",
"account.unblock_domain": "رفع مسدودیت دامنهٔ {domain}",
"account.unblock_domain_short": "آنبلاک",
"account.unblock_short": "رفع مسدودیت",
"account.unendorse": "معرّفی نکردن در نمایه",
"account.unfollow": "پی‌نگرفتن",

View file

@ -19,17 +19,19 @@
"account.block_domain": "Estä verkkotunnus {domain}",
"account.block_short": "Estä",
"account.blocked": "Estetty",
"account.blocking": "Estetty",
"account.cancel_follow_request": "Peruuta seurantapyyntö",
"account.copy": "Kopioi linkki profiiliin",
"account.direct": "Mainitse @{name} yksityisesti",
"account.disable_notifications": "Lopeta ilmoittamasta minulle, kun @{name} julkaisee",
"account.domain_blocked": "Verkkotunnus estetty",
"account.domain_blocking": "Estetty verkkotunnus",
"account.edit_profile": "Muokkaa profiilia",
"account.enable_notifications": "Ilmoita minulle, kun @{name} julkaisee",
"account.endorse": "Suosittele profiilissasi",
"account.featured.hashtags": "Aihetunnisteet",
"account.featured.posts": "Julkaisut",
"account.featured_tags.last_status_at": "Viimeisin julkaisu {date}",
"account.featured_tags.last_status_never": "Ei julkaisuja",
"account.featured_tags.title": "Käyttäjän {name} suosittelemat aihetunnisteet",
"account.follow": "Seuraa",
"account.follow_back": "Seuraa takaisin",
"account.followers": "Seuraajat",
@ -38,6 +40,7 @@
"account.following": "Seurattavat",
"account.following_counter": "{count, plural, one {{counter} seurattava} other {{counter} seurattavaa}}",
"account.follows.empty": "Tämä käyttäjä ei vielä seuraa ketään.",
"account.follows_you": "Seuraa sinua",
"account.go_to_profile": "Siirry profiiliin",
"account.hide_reblogs": "Piilota käyttäjän @{name} tehostukset",
"account.in_memoriam": "Muistoissamme.",
@ -52,6 +55,7 @@
"account.mute_notifications_short": "Mykistä ilmoitukset",
"account.mute_short": "Mykistä",
"account.muted": "Mykistetty",
"account.muting": "Hiljennetty",
"account.mutual": "Seuraatte toisianne",
"account.no_bio": "Kuvausta ei ole annettu.",
"account.open_original_page": "Avaa alkuperäinen sivu",
@ -60,6 +64,7 @@
"account.report": "Raportoi @{name}",
"account.requested": "Odottaa hyväksyntää. Peruuta seurantapyyntö napsauttamalla",
"account.requested_follow": "{name} on pyytänyt lupaa seurata sinua",
"account.requests_to_follow_you": "Pyynnöt seurata sinua",
"account.share": "Jaa käyttäjän @{name} profiili",
"account.show_reblogs": "Näytä käyttäjän @{name} tehostukset",
"account.statuses_counter": "{count, plural, one {{counter} julkaisu} other {{counter} julkaisua}}",
@ -240,7 +245,7 @@
"conversation.mark_as_read": "Merkitse luetuksi",
"conversation.open": "Näytä keskustelu",
"conversation.with": "{names} kanssa",
"copy_icon_button.copied": "Sisältö kopioitiin leikepöydälle",
"copy_icon_button.copied": "Kopioitu leikepöydälle",
"copypaste.copied": "Kopioitu",
"copypaste.copy_to_clipboard": "Kopioi leikepöydälle",
"directory.federated": "Tunnetusta fediversumista",
@ -378,6 +383,8 @@
"generic.saved": "Tallennettu",
"getting_started.heading": "Näin pääset alkuun",
"hashtag.admin_moderation": "Avaa tunnisteen #{name} moderointinäkymä",
"hashtag.browse": "Selaa julkaisuja tunnisteella #{hashtag}",
"hashtag.browse_from_account": "Selaa julkaisuja käyttäjältä @{name} tunnisteella #{hashtag}",
"hashtag.column_header.tag_mode.all": "ja {additional}",
"hashtag.column_header.tag_mode.any": "tai {additional}",
"hashtag.column_header.tag_mode.none": "ilman {additional}",
@ -391,6 +398,7 @@
"hashtag.counter_by_uses": "{count, plural, one{{counter} julkaisu} other {{counter} julkaisua}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} julkaisu} other {{counter} julkaisua}} tänään",
"hashtag.follow": "Seuraa aihetunnistetta",
"hashtag.mute": "Mykistä #{hashtag}",
"hashtag.unfollow": "Lopeta aihetunnisteen seuraaminen",
"hashtags.and_other": "…ja {count, plural, other {# lisää}}",
"hints.profiles.followers_may_be_missing": "Tämän profiilin seuraajia saattaa puuttua.",

View file

@ -23,13 +23,11 @@
"account.copy": "I-sipi ang kawing sa profile",
"account.direct": "Palihim banggitin si @{name}",
"account.disable_notifications": "I-tigil ang pagpapaalam sa akin tuwing nagpopost si @{name}",
"account.domain_blocked": "Hinadlangan ang domain",
"account.edit_profile": "Baguhin ang profile",
"account.enable_notifications": "Ipaalam sa akin kapag nag-post si @{name}",
"account.endorse": "I-tampok sa profile",
"account.featured_tags.last_status_at": "Huling post noong {date}",
"account.featured_tags.last_status_never": "Walang mga post",
"account.featured_tags.title": "Nakatampok na hashtag ni {name}",
"account.follow": "Sundan",
"account.follow_back": "Sundan pabalik",
"account.followers": "Mga tagasunod",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "I-mute ang mga abiso",
"account.mute_short": "I-mute",
"account.muted": "Naka-mute",
"account.mutual": "Ka-mutual",
"account.no_bio": "Walang nakalaan na paglalarawan.",
"account.open_original_page": "Buksan ang pinagmulang pahina",
"account.posts": "Mga post",

View file

@ -19,17 +19,20 @@
"account.block_domain": "Banna økisnavnið {domain}",
"account.block_short": "Blokera",
"account.blocked": "Bannað/ur",
"account.blocking": "Banni",
"account.cancel_follow_request": "Strika fylgjaraumbøn",
"account.copy": "Avrita leinki til vangan",
"account.direct": "Umrøð @{name} privat",
"account.disable_notifications": "Ikki boða mær frá, tá @{name} skrivar",
"account.domain_blocked": "Økisnavn bannað",
"account.domain_blocking": "Banni økisnavn",
"account.edit_profile": "Broyt vanga",
"account.enable_notifications": "Boða mær frá, tá @{name} skrivar",
"account.endorse": "Víst á vangamyndini",
"account.featured": "Tikin fram",
"account.featured.hashtags": "Frámerki",
"account.featured.posts": "Postar",
"account.featured_tags.last_status_at": "Seinasta strongur skrivaður {date}",
"account.featured_tags.last_status_never": "Einki uppslag",
"account.featured_tags.title": "Tvíkrossar hjá {name}",
"account.follow": "Fylg",
"account.follow_back": "Fylg aftur",
"account.followers": "Fylgjarar",
@ -38,6 +41,7 @@
"account.following": "Fylgir",
"account.following_counter": "{count, plural, one {{counter} fylgir} other {{counter} fylgja}}",
"account.follows.empty": "Hesin brúkari fylgir ongum enn.",
"account.follows_you": "Fylgir tær",
"account.go_to_profile": "Far til vanga",
"account.hide_reblogs": "Fjal lyft frá @{name}",
"account.in_memoriam": "In memoriam.",
@ -52,7 +56,8 @@
"account.mute_notifications_short": "Sløkk fráboðanir",
"account.mute_short": "Doyv",
"account.muted": "Sløkt/ur",
"account.mutual": "Sínamillum",
"account.muting": "Doyvir",
"account.mutual": "Tit fylgja hvønn annan",
"account.no_bio": "Lýsing vantar.",
"account.open_original_page": "Opna upprunasíðuna",
"account.posts": "Uppsløg",
@ -60,6 +65,7 @@
"account.report": "Melda @{name}",
"account.requested": "Bíðar eftir góðkenning. Trýst fyri at angra umbønina",
"account.requested_follow": "{name} hevur biðið um at fylgja tær",
"account.requests_to_follow_you": "Umbønir um at fylgja tær",
"account.share": "Deil vanga @{name}'s",
"account.show_reblogs": "Vís lyft frá @{name}",
"account.statuses_counter": "{count, plural, one {{counter} postur} other {{counter} postar}}",
@ -294,6 +300,9 @@
"emoji_button.search_results": "Leitiúrslit",
"emoji_button.symbols": "Ímyndir",
"emoji_button.travel": "Ferðing og støð",
"empty_column.account_featured.me": "Tú hevur ikki tikið nakað fram enn. Visti tú, at tú kanst taka fram egnar postar, frámerki, tú brúkar mest, og sjálvt kontur hjá vinum tínum á vangan hjá tær?",
"empty_column.account_featured.other": "{acct} hevur ikki tikið nakað fram enn. Visti tú, at tú kanst taka fram tínar postar, frámerki, tú brúkar mest, og sjálvt kontur hjá vinum tínum á vangan hjá tær?",
"empty_column.account_featured_other.unknown": "Hendan kontan hevur ikki tikið nakað fram enn.",
"empty_column.account_hides_collections": "Hesin brúkarin hevur valt, at hesar upplýsingarnar ikki skulu vera tøkar",
"empty_column.account_suspended": "Kontan gjørd óvirkin",
"empty_column.account_timeline": "Einki uppslag her!",
@ -378,6 +387,8 @@
"generic.saved": "Goymt",
"getting_started.heading": "At byrja",
"hashtag.admin_moderation": "Lat umsjónarmarkamót upp fyri #{name}",
"hashtag.browse": "Blaða gjøgnum postar í #{hashtag}",
"hashtag.browse_from_account": "Blaða gjøgnum postar frá @{name} í #{hashtag}",
"hashtag.column_header.tag_mode.all": "og {additional}",
"hashtag.column_header.tag_mode.any": "ella {additional}",
"hashtag.column_header.tag_mode.none": "uttan {additional}",
@ -391,6 +402,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} postur} other {{counter} postar}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} postur} other {{counter} postar}} í dag",
"hashtag.follow": "Fylg frámerki",
"hashtag.mute": "Doyv @#{hashtag}",
"hashtag.unfollow": "Gevst at fylgja frámerki",
"hashtags.and_other": "…og {count, plural, other {# afturat}}",
"hints.profiles.followers_may_be_missing": "Fylgjarar hjá hesum vanganum kunnu mangla.",

View file

@ -23,13 +23,14 @@
"account.copy": "Copier le lien vers le profil",
"account.direct": "Mention privée @{name}",
"account.disable_notifications": "Ne plus me notifier quand @{name} publie",
"account.domain_blocked": "Domaine bloqué",
"account.edit_profile": "Modifier le profil",
"account.enable_notifications": "Me notifier quand @{name} publie",
"account.endorse": "Inclure sur profil",
"account.featured": "En vedette",
"account.featured.hashtags": "Hashtags",
"account.featured.posts": "Messages",
"account.featured_tags.last_status_at": "Dernière publication {date}",
"account.featured_tags.last_status_never": "Aucune publication",
"account.featured_tags.title": "Hashtags inclus de {name}",
"account.follow": "Suivre",
"account.follow_back": "Suivre en retour",
"account.followers": "abonné·e·s",
@ -52,7 +53,6 @@
"account.mute_notifications_short": "Rendre les notifications muettes",
"account.mute_short": "Rendre muet",
"account.muted": "Masqué·e",
"account.mutual": "Mutuel",
"account.no_bio": "Description manquante.",
"account.open_original_page": "Ouvrir la page d'origine",
"account.posts": "Publications",
@ -65,6 +65,7 @@
"account.statuses_counter": "{count, plural, one {{counter} message} other {{counter} messages}}",
"account.unblock": "Débloquer @{name}",
"account.unblock_domain": "Débloquer le domaine {domain}",
"account.unblock_domain_short": "Débloquer",
"account.unblock_short": "Débloquer",
"account.unendorse": "Ne pas inclure sur profil",
"account.unfollow": "Ne plus suivre",
@ -905,6 +906,10 @@
"video.expand": "Agrandir la vidéo",
"video.fullscreen": "Plein écran",
"video.hide": "Masquer la vidéo",
"video.mute": "Couper le son",
"video.pause": "Pause",
"video.play": "Lecture"
"video.play": "Lecture",
"video.unmute": "Rétablir le son",
"video.volume_down": "Baisser le volume",
"video.volume_up": "Augmenter le volume"
}

View file

@ -23,13 +23,14 @@
"account.copy": "Copier le lien vers le profil",
"account.direct": "Mention privée @{name}",
"account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose",
"account.domain_blocked": "Domaine bloqué",
"account.edit_profile": "Modifier le profil",
"account.enable_notifications": "Me notifier quand @{name} publie quelque chose",
"account.endorse": "Recommander sur votre profil",
"account.featured": "En vedette",
"account.featured.hashtags": "Hashtags",
"account.featured.posts": "Messages",
"account.featured_tags.last_status_at": "Dernier message le {date}",
"account.featured_tags.last_status_never": "Aucun message",
"account.featured_tags.title": "Les hashtags en vedette de {name}",
"account.follow": "Suivre",
"account.follow_back": "Suivre en retour",
"account.followers": "Abonné·e·s",
@ -52,7 +53,6 @@
"account.mute_notifications_short": "Désactiver les notifications",
"account.mute_short": "Mettre en sourdine",
"account.muted": "Masqué·e",
"account.mutual": "Mutuel",
"account.no_bio": "Aucune description fournie.",
"account.open_original_page": "Ouvrir la page d'origine",
"account.posts": "Messages",
@ -65,6 +65,7 @@
"account.statuses_counter": "{count, plural, one {{counter} message} other {{counter} messages}}",
"account.unblock": "Débloquer @{name}",
"account.unblock_domain": "Débloquer le domaine {domain}",
"account.unblock_domain_short": "Débloquer",
"account.unblock_short": "Débloquer",
"account.unendorse": "Ne plus recommander sur le profil",
"account.unfollow": "Ne plus suivre",
@ -905,6 +906,10 @@
"video.expand": "Agrandir la vidéo",
"video.fullscreen": "Plein écran",
"video.hide": "Masquer la vidéo",
"video.mute": "Couper le son",
"video.pause": "Pause",
"video.play": "Lecture"
"video.play": "Lecture",
"video.unmute": "Rétablir le son",
"video.volume_down": "Baisser le volume",
"video.volume_up": "Augmenter le volume"
}

View file

@ -23,13 +23,11 @@
"account.copy": "Keppeling nei profyl kopiearje",
"account.direct": "Privee fermelde @{name}",
"account.disable_notifications": "Jou gjin melding mear wannear @{name} in berjocht pleatst",
"account.domain_blocked": "Domein blokkearre",
"account.edit_profile": "Profyl bewurkje",
"account.enable_notifications": "Jou in melding mear wannear @{name} in berjocht pleatst",
"account.endorse": "Op profyl werjaan",
"account.featured_tags.last_status_at": "Lêste berjocht op {date}",
"account.featured_tags.last_status_never": "Gjin berjochten",
"account.featured_tags.title": "Utljochte hashtags fan {name}",
"account.follow": "Folgje",
"account.follow_back": "Weromfolgje",
"account.followers": "Folgers",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "Meldingen negearje",
"account.mute_short": "Negearje",
"account.muted": "Negearre",
"account.mutual": "Jimme folgje inoar",
"account.no_bio": "Gjin omskriuwing opjûn.",
"account.open_original_page": "Orizjinele side iepenje",
"account.posts": "Berjochten",

View file

@ -23,13 +23,11 @@
"account.copy": "Cóipeáil nasc chuig an bpróifíl",
"account.direct": "Luaigh @{name} go príobháideach",
"account.disable_notifications": "Éirigh as ag cuir mé in eol nuair bpostálann @{name}",
"account.domain_blocked": "Ainm fearainn bactha",
"account.edit_profile": "Cuir an phróifíl in eagar",
"account.enable_notifications": "Cuir mé in eol nuair bpostálann @{name}",
"account.endorse": "Cuir ar an phróifíl mar ghné",
"account.featured_tags.last_status_at": "Postáil is déanaí ar {date}",
"account.featured_tags.last_status_never": "Gan aon phoist",
"account.featured_tags.title": "Haischlib faoi thrácht {name}",
"account.follow": "Lean",
"account.follow_back": "Leanúint ar ais",
"account.followers": "Leantóirí",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "Balbhaigh fógraí",
"account.mute_short": "Balbhaigh",
"account.muted": "Balbhaithe",
"account.mutual": "Frithpháirteach",
"account.no_bio": "Níor tugadh tuairisc.",
"account.open_original_page": "Oscail an leathanach bunaidh",
"account.posts": "Postálacha",
@ -65,6 +62,7 @@
"account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} poist}}",
"account.unblock": "Bain bac de @{name}",
"account.unblock_domain": "Bain bac den ainm fearainn {domain}",
"account.unblock_domain_short": "Díbhlocáil",
"account.unblock_short": "Díbhlocáil",
"account.unendorse": "Ná chuir ar an phróifíl mar ghné",
"account.unfollow": "Ná lean a thuilleadh",
@ -905,6 +903,12 @@
"video.expand": "Leath físeán",
"video.fullscreen": "Lánscáileán",
"video.hide": "Cuir físeán i bhfolach",
"video.mute": "Balbhaigh",
"video.pause": "Cuir ar sos",
"video.play": "Cuir ar siúl"
"video.play": "Cuir ar siúl",
"video.skip_backward": "Scipeáil siar",
"video.skip_forward": "Scipeáil ar aghaidh",
"video.unmute": "Díbhalbhú",
"video.volume_down": "Toirt síos",
"video.volume_up": "Toirt suas"
}

View file

@ -23,13 +23,11 @@
"account.copy": "Dèan lethbhreac dhen cheangal dhan phròifil",
"account.direct": "Thoir iomradh air @{name} gu prìobhaideach",
"account.disable_notifications": "Na cuir brath thugam tuilleadh nuair a chuireas @{name} post ris",
"account.domain_blocked": "Chaidh an àrainn a bhacadh",
"account.edit_profile": "Deasaich a phròifil",
"account.enable_notifications": "Cuir brath thugam nuair a chuireas @{name} post ris",
"account.endorse": "Brosnaich air a phròifil",
"account.featured_tags.last_status_at": "Am post mu dheireadh {date}",
"account.featured_tags.last_status_never": "Gun phost",
"account.featured_tags.title": "Na tagaichean hais brosnaichte aig {name}",
"account.follow": "Lean",
"account.follow_back": "Lean air ais",
"account.followers": "Luchd-leantainn",
@ -52,7 +50,6 @@
"account.mute_notifications_short": "Mùch na brathan",
"account.mute_short": "Mùch",
"account.muted": "Ga mhùchadh",
"account.mutual": "Co-dhàimh",
"account.no_bio": "Cha deach tuairisgeul a sholar.",
"account.open_original_page": "Fosgail an duilleag thùsail",
"account.posts": "Postaichean",

View file

@ -23,13 +23,14 @@
"account.copy": "Copiar ligazón ao perfil",
"account.direct": "Mencionar de xeito privado a @{name}",
"account.disable_notifications": "Deixar de notificarme cando @{name} publica",
"account.domain_blocked": "Dominio agochado",
"account.edit_profile": "Editar perfil",
"account.enable_notifications": "Noficarme cando @{name} publique",
"account.endorse": "Amosar no perfil",
"account.featured": "Destacado",
"account.featured.hashtags": "Cancelos",
"account.featured.posts": "Publicacións",
"account.featured_tags.last_status_at": "Última publicación o {date}",
"account.featured_tags.last_status_never": "Sen publicacións",
"account.featured_tags.title": "Cancelos destacados de {name}",
"account.follow": "Seguir",
"account.follow_back": "Seguir tamén",
"account.followers": "Seguidoras",
@ -52,7 +53,6 @@
"account.mute_notifications_short": "Silenciar notificacións",
"account.mute_short": "Acalar",
"account.muted": "Acalada",
"account.mutual": "Mutuo",
"account.no_bio": "Sen descrición.",
"account.open_original_page": "Abrir páxina orixinal",
"account.posts": "Publicacións",
@ -378,6 +378,8 @@
"generic.saved": "Gardado",
"getting_started.heading": "Primeiros pasos",
"hashtag.admin_moderation": "Abrir interface de moderación para ##{name}",
"hashtag.browse": "Ver publicacións con #{hashtag}",
"hashtag.browse_from_account": "Ver as publicacións de @{name} con #{hashtag}",
"hashtag.column_header.tag_mode.all": "e {additional}",
"hashtag.column_header.tag_mode.any": "ou {additional}",
"hashtag.column_header.tag_mode.none": "sen {additional}",
@ -391,6 +393,7 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicacións}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicación} other {{counter} publicacións}} hoxe",
"hashtag.follow": "Seguir cancelo",
"hashtag.mute": "Acalar a #{hashtag}",
"hashtag.unfollow": "Deixar de seguir cancelo",
"hashtags.and_other": "…e {count, plural, one {}other {# máis}}",
"hints.profiles.followers_may_be_missing": "Poderían faltar seguidoras deste perfil.",

View file

@ -19,17 +19,20 @@
"account.block_domain": "חסמו את קהילת {domain}",
"account.block_short": "לחסום",
"account.blocked": "לחסום",
"account.blocking": "רשימת החשבונות החסומים",
"account.cancel_follow_request": "משיכת בקשת מעקב",
"account.copy": "להעתיק קישור לפרופיל",
"account.direct": "הודעה פרטית אל @{name}",
"account.disable_notifications": "הפסק לשלוח לי התראות כש@{name} מפרסמים",
"account.domain_blocked": "הדומיין חסום",
"account.domain_blocking": "רשימת השרתים החסומים",
"account.edit_profile": "עריכת פרופיל",
"account.enable_notifications": "שלח לי התראות כש@{name} מפרסם",
"account.endorse": "קדם את החשבון בפרופיל",
"account.featured": "מומלץ",
"account.featured.hashtags": "תגיות",
"account.featured.posts": "הודעות",
"account.featured_tags.last_status_at": "חצרוץ אחרון בתאריך {date}",
"account.featured_tags.last_status_never": "אין חצרוצים",
"account.featured_tags.title": "התגיות המועדפות של {name}",
"account.follow": "לעקוב",
"account.follow_back": "לעקוב בחזרה",
"account.followers": "עוקבים",
@ -38,6 +41,7 @@
"account.following": "נעקבים",
"account.following_counter": "{count, plural,one {עוקב אחרי {count}}other {עוקב אחרי {counter}}}",
"account.follows.empty": "משתמש זה עדיין לא עוקב אחרי אף אחד.",
"account.follows_you": "במעקב אחריך",
"account.go_to_profile": "מעבר לפרופיל",
"account.hide_reblogs": "להסתיר הידהודים מאת @{name}",
"account.in_memoriam": "פרופיל זכרון.",
@ -52,7 +56,8 @@
"account.mute_notifications_short": "השתקת התראות",
"account.mute_short": "השתקה",
"account.muted": "מושתק",
"account.mutual": "הדדיים",
"account.muting": "רשימת החשבונות המושתקים",
"account.mutual": "אתם עוקביםות הדדית",
"account.no_bio": "לא סופק תיאור.",
"account.open_original_page": "לפתיחת העמוד המקורי",
"account.posts": "פוסטים",
@ -60,6 +65,7 @@
"account.report": "דווח על @{name}",
"account.requested": "בהמתנה לאישור. לחצי כדי לבטל בקשת מעקב",
"account.requested_follow": "{name} ביקשו לעקוב אחריך",
"account.requests_to_follow_you": "ביקשו לעקוב אחריך",
"account.share": "שתף את הפרופיל של @{name}",
"account.show_reblogs": "הצג הדהודים מאת @{name}",
"account.statuses_counter": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}}",
@ -294,6 +300,9 @@
"emoji_button.search_results": "תוצאות חיפוש",
"emoji_button.symbols": "סמלים",
"emoji_button.travel": "טיולים ואתרים",
"empty_column.account_featured.me": "עוד לא קידמת תכנים. הידעת שניתן לקדם תכני הודעות, תגיות שבשימושך התדיר או אפילו את החשבונות של חבריםות בפרופיל שלך?",
"empty_column.account_featured.other": "{acct} עוד לא קידם תכנים. הידעת שניתן לקדם תכני הודעות, תגיות שבשימושך התדיר או אפילו את החשבונות של חבריםות בפרופיל שלך?",
"empty_column.account_featured_other.unknown": "חשבון זה עוד לא קידם תכנים.",
"empty_column.account_hides_collections": "המשתמש.ת בחר.ה להסתיר מידע זה",
"empty_column.account_suspended": "חשבון מושעה",
"empty_column.account_timeline": "אין עדיין אף הודעה!",
@ -378,6 +387,8 @@
"generic.saved": "נשמר",
"getting_started.heading": "בואו נתחיל",
"hashtag.admin_moderation": "פתיחת ממשק פיקוח דיון עבור #{name}",
"hashtag.browse": "קריאת הודעות תחת #{hashtag}",
"hashtag.browse_from_account": "קריאת הודעות מאת @{name} תחת #{hashtag}",
"hashtag.column_header.tag_mode.all": "ו- {additional}",
"hashtag.column_header.tag_mode.any": "או {additional}",
"hashtag.column_header.tag_mode.none": "ללא {additional}",
@ -391,6 +402,7 @@
"hashtag.counter_by_uses": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}}",
"hashtag.counter_by_uses_today": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}} היום",
"hashtag.follow": "לעקוב אחרי תגית",
"hashtag.mute": "להשתיק את #{hashtag}",
"hashtag.unfollow": "להפסיק לעקוב אחרי תגית",
"hashtags.and_other": "…{count, plural,other {ועוד #}}",
"hints.profiles.followers_may_be_missing": "יתכן כי עוקבים של פרופיל זה חסרים.",

View file

@ -22,13 +22,11 @@
"account.copy": "प्रोफाइल पर लिंक कॉपी करें",
"account.direct": "निजि तरीके से उल्लेख करे @{name}",
"account.disable_notifications": "@{name} पोस्ट के लिए मुझे सूचित मत करो",
"account.domain_blocked": "छिपा हुआ डोमेन",
"account.edit_profile": "प्रोफ़ाइल संपादित करें",
"account.enable_notifications": "जब @{name} पोस्ट मौजूद हो सूचित करें",
"account.endorse": "प्रोफ़ाइल पर दिखाए",
"account.featured_tags.last_status_at": "{date} का अंतिम पोस्ट",
"account.featured_tags.last_status_never": "कोई पोस्ट नहीं है",
"account.featured_tags.title": "{name} के चुनिंदा हैशटैग",
"account.follow": "फॉलो करें",
"account.follow_back": "फॉलो करें",
"account.followers": "फॉलोवर",
@ -49,7 +47,6 @@
"account.mute_notifications_short": "सूचनाओ को शांत करे",
"account.mute_short": "शांत करे",
"account.muted": "म्यूट है",
"account.mutual": "आपसी",
"account.no_bio": "कोई विवरण नहि दिया गया हे",
"account.open_original_page": "ओरिजिनल पोस्ट खोलें",
"account.posts": "टूट्स",

View file

@ -22,13 +22,11 @@
"account.copy": "Kopiraj vezu u profil",
"account.direct": "Privatno spomeni @{name}",
"account.disable_notifications": "Nemoj me obavjestiti kada @{name} napravi objavu",
"account.domain_blocked": "Domena je blokirana",
"account.edit_profile": "Uredi profil",
"account.enable_notifications": "Obavjesti me kada @{name} napravi objavu",
"account.endorse": "Istakni na profilu",
"account.featured_tags.last_status_at": "Zadnji post {date}",
"account.featured_tags.last_status_never": "Nema postova",
"account.featured_tags.title": "Istaknuti hashtagovi {name}",
"account.follow": "Prati",
"account.follow_back": "Slijedi natrag",
"account.followers": "Pratitelji",
@ -48,7 +46,6 @@
"account.mute_notifications_short": "Utišaj obavijesti",
"account.mute_short": "Utišaj",
"account.muted": "Utišano",
"account.mutual": "Uzajamno",
"account.no_bio": "Nije dan opis.",
"account.open_original_page": "Otvori originalnu stranicu",
"account.posts": "Objave",

Some files were not shown because too many files have changed in this diff Show more