Merge commit '9e04007020' into upstream-20240725

This commit is contained in:
KMY 2024-07-25 13:06:26 +09:00
commit a99f174d98
322 changed files with 8093 additions and 1586 deletions

View file

@ -88,7 +88,7 @@ gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'simple_form', '~> 5.2'
gem 'simple-navigation', '~> 4.4'
gem 'stoplight', '~> 4.1'
gem 'strong_migrations', '1.8.0'
gem 'strong_migrations'
gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023'

View file

@ -222,16 +222,16 @@ GEM
elasticsearch-transport (7.17.10)
faraday (>= 1, < 3)
multi_json
email_spec (2.2.2)
email_spec (2.3.0)
htmlentities (~> 4.3.3)
launchy (~> 2.1)
launchy (>= 2.1, < 4.0)
mail (~> 2.7)
erubi (1.13.0)
et-orbi (1.2.11)
tzinfo
excon (0.110.0)
fabrication (2.31.0)
faker (3.4.1)
faker (3.4.2)
i18n (>= 1.8.11, < 2)
faraday (1.10.3)
faraday-em_http (~> 1.0)
@ -367,7 +367,7 @@ GEM
json-ld-preloaded (3.3.0)
json-ld (~> 3.3)
rdf (~> 3.3)
json-schema (4.3.0)
json-schema (4.3.1)
addressable (>= 2.8)
jsonapi-renderer (0.2.2)
jwt (2.7.1)
@ -440,7 +440,7 @@ GEM
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.12)
net-imap (0.4.14)
date
net-protocol
net-ldap (0.19.0)
@ -512,14 +512,14 @@ GEM
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (~> 0.21)
opentelemetry-instrumentation-action_view (0.7.0)
opentelemetry-instrumentation-action_view (0.7.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_job (0.7.2)
opentelemetry-instrumentation-active_job (0.7.3)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_model_serializers (0.20.1)
opentelemetry-instrumentation-active_model_serializers (0.20.2)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_record (0.7.2)
@ -531,32 +531,32 @@ GEM
opentelemetry-instrumentation-base (0.22.3)
opentelemetry-api (~> 1.0)
opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.21.3)
opentelemetry-instrumentation-concurrent_ruby (0.21.4)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-excon (0.22.3)
opentelemetry-instrumentation-excon (0.22.4)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-faraday (0.24.5)
opentelemetry-instrumentation-faraday (0.24.6)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-http (0.23.3)
opentelemetry-instrumentation-http (0.23.4)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-http_client (0.22.6)
opentelemetry-instrumentation-http_client (0.22.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-net_http (0.22.6)
opentelemetry-instrumentation-net_http (0.22.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-pg (0.27.3)
opentelemetry-instrumentation-pg (0.27.4)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (0.24.5)
opentelemetry-instrumentation-rack (0.24.6)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rails (0.31.0)
opentelemetry-instrumentation-rails (0.31.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
opentelemetry-instrumentation-action_pack (~> 0.9.0)
@ -565,10 +565,10 @@ GEM
opentelemetry-instrumentation-active_record (~> 0.7.0)
opentelemetry-instrumentation-active_support (~> 0.6.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-redis (0.25.6)
opentelemetry-instrumentation-redis (0.25.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-sidekiq (0.25.6)
opentelemetry-instrumentation-sidekiq (0.25.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-registry (0.3.1)
@ -607,7 +607,7 @@ GEM
railties (>= 7.0.0)
psych (5.1.2)
stringio
public_suffix (6.0.0)
public_suffix (6.0.1)
puma (6.4.2)
nio4r (~> 2.0)
pundit (2.3.2)
@ -696,7 +696,7 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.3.1)
rexml (3.3.2)
strscan
rotp (6.3.0)
rouge (4.2.1)
@ -766,8 +766,9 @@ GEM
ruby-saml (1.16.0)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.2.1)
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
rufus-scheduler (3.9.1)
@ -780,7 +781,7 @@ GEM
scenic (1.8.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
selenium-webdriver (4.22.0)
selenium-webdriver (4.23.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@ -820,8 +821,8 @@ GEM
stoplight (4.1.0)
redlock (~> 1.0)
stringio (3.1.1)
strong_migrations (1.8.0)
activerecord (>= 5.2)
strong_migrations (2.0.0)
activerecord (>= 6.1)
strscan (3.1.0)
swd (1.3.0)
activesupport (>= 3)
@ -893,7 +894,7 @@ GEM
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.8.1)
websocket (1.2.10)
websocket (1.2.11)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
@ -1045,7 +1046,7 @@ DEPENDENCIES
simplecov-lcov (~> 0.8)
stackprof
stoplight (~> 4.1)
strong_migrations (= 1.8.0)
strong_migrations
test-prof
thor (~> 1.2)
tty-prompt (~> 0.23)

View file

@ -13,6 +13,7 @@ module Admin
def show
authorize :instance, :show?
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
end
def destroy

View file

@ -47,18 +47,13 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
private
def set_domain_allows
@domain_allows = filtered_domain_allows.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
@domain_allows = DomainAllow.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_domain_allow
@domain_allow = DomainAllow.find(params[:id])
end
def filtered_domain_allows
# TODO: no filtering yet
DomainAllow.all
end
def next_path
api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end

View file

@ -59,18 +59,13 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
end
def set_domain_blocks
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
@domain_blocks = DomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_domain_block
@domain_block = DomainBlock.find(params[:id])
end
def filtered_domain_blocks
# TODO: no filtering yet
DomainBlock.all
end
def domain_block_params
params.permit(:severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, :reject_reports, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow,
:reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden)

View file

@ -10,7 +10,7 @@ class Api::V1::ReportsController < Api::BaseController
@report = ReportService.new.call(
current_account,
reported_account,
report_params
report_params.merge(application: doorkeeper_token.application)
)
render json: @report, serializer: REST::ReportSerializer

View file

@ -12,10 +12,27 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
with_read_replica do
@notifications = load_notifications
@group_metadata = load_group_metadata
@grouped_notifications = load_grouped_notifications
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
@sample_accounts = @grouped_notifications.flat_map(&:sample_accounts)
# Preload associations to avoid N+1s
ActiveRecord::Associations::Preloader.new(records: @sample_accounts, associations: [:account_stat, { user: :role }]).call
end
render json: @notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#index rendering') do |span|
statuses = @grouped_notifications.filter_map { |group| group.target_status&.id }
span.add_attributes(
'app.notification_grouping.count' => @grouped_notifications.size,
'app.notification_grouping.sample_account.count' => @sample_accounts.size,
'app.notification_grouping.sample_account.unique_count' => @sample_accounts.pluck(:id).uniq.size,
'app.notification_grouping.status.count' => statuses.size,
'app.notification_grouping.status.unique_count' => statuses.uniq.size
)
render json: @grouped_notifications, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata
end
end
def show
@ -36,25 +53,35 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
private
def load_notifications
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_notifications') do
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
preload_collection(target_statuses, Status)
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
preload_collection(target_statuses, Status)
end
end
end
def load_group_metadata
return {} if @notifications.empty?
browserable_account_notifications
.where(group_key: @notifications.filter_map(&:group_key))
.where(id: (@notifications.last.id)..(@notifications.first.id))
.group(:group_key)
.pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at')
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] }
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_group_metadata') do
browserable_account_notifications
.where(group_key: @notifications.filter_map(&:group_key))
.where(id: (@notifications.last.id)..(@notifications.first.id))
.group(:group_key)
.pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at')
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] }
end
end
def load_grouped_notifications
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do
@notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }
end
end
def browserable_account_notifications

View file

@ -316,8 +316,8 @@ function loaded() {
const message =
statusEl.dataset.spoiler === 'expanded'
? localeData['status.show_less'] ?? 'Show less'
: localeData['status.show_more'] ?? 'Show more';
? (localeData['status.show_less'] ?? 'Show less')
: (localeData['status.show_more'] ?? 'Show more');
spoilerLink.textContent = new IntlMessageFormat(
message,
locale,

View file

@ -1,3 +1,5 @@
import { browserHistory } from 'mastodon/components/router';
import api, { getLinks } from '../api';
import {
@ -676,3 +678,13 @@ export const updateAccount = ({ displayName, note, avatar, header, discoverable,
dispatch(importFetchedAccount(response.data));
});
};
export const navigateToProfile = (accountId) => {
return (_dispatch, getState) => {
const acct = getState().accounts.getIn([accountId, 'acct']);
if (acct) {
browserHistory.push(`/@${acct}`);
}
};
};

View file

@ -4,6 +4,7 @@ import axios from 'axios';
import { throttle } from 'lodash';
import api from 'mastodon/api';
import { browserHistory } from 'mastodon/components/router';
import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'mastodon/settings';
@ -97,9 +98,9 @@ const messages = defineMessages({
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
});
export const ensureComposeIsVisible = (getState, routerHistory) => {
export const ensureComposeIsVisible = (getState) => {
if (!getState().getIn(['compose', 'mounted'])) {
routerHistory.push('/publish');
browserHistory.push('/publish');
}
};
@ -119,14 +120,26 @@ export function changeCompose(text) {
};
}
export function replyCompose(status, routerHistory) {
export function replyCompose(status) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_REPLY,
status: status,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
}
export function replyComposeById(statusId) {
return (dispatch, getState) => {
const state = getState();
const status = state.statuses.get(statusId);
if (status) {
const account = state.accounts.get(status.get('account'));
dispatch(replyCompose(status.set('account', account)));
}
};
}
@ -142,38 +155,44 @@ export function resetCompose() {
};
}
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
export const focusCompose = (defaultText) => (dispatch, getState) => {
dispatch({
type: COMPOSE_FOCUS,
defaultText,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
export function mentionCompose(account, routerHistory) {
export function mentionCompose(account) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_MENTION,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
}
export function directCompose(account, routerHistory) {
export function mentionComposeById(accountId) {
return (dispatch, getState) => {
dispatch(mentionCompose(getState().accounts.get(accountId)));
};
}
export function directCompose(account) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_DIRECT,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
}
export function submitCompose(routerHistory) {
export function submitCompose() {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
@ -228,8 +247,8 @@ export function submitCompose(routerHistory) {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
if (routerHistory && (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new') && window.history.state) {
routerHistory.goBack();
if ((browserHistory.location.pathname === '/publish' || browserHistory.location.pathname === '/statuses/new') && window.history.state) {
browserHistory.goBack();
}
dispatch(insertIntoTagHistory(response.data.tags, status));
@ -267,7 +286,7 @@ export function submitCompose(routerHistory) {
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
onClick: () => browserHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}).catch(function (error) {
dispatch(submitComposeFail(error));

View file

@ -1,7 +1,11 @@
import { boostModal } from 'mastodon/initial_state';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
import { unreblog, reblog } from './interactions_typed';
import { openModal } from './modal';
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
@ -741,3 +745,49 @@ export function expandMentionedUsersFail(id, error) {
error,
};
}
function toggleReblogWithoutConfirmation(status, privacy) {
return (dispatch) => {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
dispatch(reblog({ statusId: status.get('id'), privacy }));
}
};
}
export function toggleReblog(statusId, skipModal = false, forceModal = false) {
return (dispatch, getState) => {
const state = getState();
let status = state.statuses.get(statusId);
if (!status)
return;
// The reblog modal expects a pre-filled account in status
// TODO: fix this by having the reblog modal get a statusId and do the work itself
status = status.set('account', state.accounts.get(status.get('account')));
if ((boostModal && !skipModal) || (forceModal && !status.get('reblogged'))) {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: (status, privacy) => dispatch(toggleReblogWithoutConfirmation(status, privacy)) } }));
} else {
dispatch(toggleReblogWithoutConfirmation(status));
}
};
}
export function toggleFavourite(statusId) {
return (dispatch, getState) => {
const state = getState();
const status = state.statuses.get(statusId);
if (!status)
return;
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
};
}

View file

@ -75,9 +75,17 @@ interface MarkerParam {
}
function getLastNotificationId(state: RootState): string | undefined {
// @ts-expect-error state.notifications is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return state.getIn(['notifications', 'lastReadId']);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const enableBeta = state.settings.getIn(
['notifications', 'groupingBeta'],
false,
) as boolean;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return enableBeta
? state.notificationGroups.lastReadId
: // @ts-expect-error state.notifications is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
state.getIn(['notifications', 'lastReadId']);
}
const buildPostMarkersParams = (state: RootState) => {

View file

@ -0,0 +1,147 @@
import { createAction } from '@reduxjs/toolkit';
import {
apiClearNotifications,
apiFetchNotifications,
} from 'mastodon/api/notifications';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
} from 'mastodon/api_types/notifications';
import { allNotificationTypes } from 'mastodon/api_types/notifications';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
import {
selectSettingsNotificationsExcludedTypes,
selectSettingsNotificationsQuickFilterActive,
} from 'mastodon/selectors/settings';
import type { AppDispatch } from 'mastodon/store';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from 'mastodon/store/typed_functions';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings';
function excludeAllTypesExcept(filter: string) {
return allNotificationTypes.filter((item) => item !== filter);
}
function dispatchAssociatedRecords(
dispatch: AppDispatch,
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];
notifications.forEach((notification) => {
if ('sample_accounts' in notification) {
fetchedAccounts.push(...notification.sample_accounts);
}
if (notification.type === 'admin.report') {
fetchedAccounts.push(notification.report.target_account);
}
if (notification.type === 'moderation_warning') {
fetchedAccounts.push(notification.moderation_warning.target_account);
}
if (
'status' in notification &&
(notification.status as ApiStatusJSON | null) !== null
) {
fetchedStatuses.push(notification.status);
}
});
if (fetchedAccounts.length > 0)
dispatch(importFetchedAccounts(fetchedAccounts));
if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
}
export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) => {
const activeFilter =
selectSettingsNotificationsQuickFilterActive(getState());
return apiFetchNotifications({
exclude_types:
activeFilter === 'all'
? selectSettingsNotificationsExcludedTypes(getState())
: excludeAllTypesExcept(activeFilter),
});
},
({ notifications }, { dispatch }) => {
dispatchAssociatedRecords(dispatch, notifications);
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
notifications;
// TODO: might be worth not using gaps for that…
// if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
if (notifications.length > 1)
payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id });
return payload;
// dispatch(submitMarkers());
},
);
export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }) =>
apiFetchNotifications({ max_id: params.gap.maxId }),
({ notifications }, { dispatch }) => {
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
},
);
export const processNewNotificationForGroups = createAppAsyncThunk(
'notificationGroups/processNew',
(notification: ApiNotificationJSON, { dispatch }) => {
dispatchAssociatedRecords(dispatch, [notification]);
return notification;
},
);
export const loadPending = createAction('notificationGroups/loadPending');
export const updateScrollPosition = createAction<{ top: boolean }>(
'notificationGroups/updateScrollPosition',
);
export const setNotificationsFilter = createAppAsyncThunk(
'notifications/filter/set',
({ filterType }: { filterType: string }, { dispatch }) => {
dispatch({
type: NOTIFICATIONS_FILTER_SET,
path: ['notifications', 'quickFilter', 'active'],
value: filterType,
});
// dispatch(expandNotifications({ forceLoad: true }));
void dispatch(fetchNotifications());
dispatch(saveSettings());
},
);
export const clearNotifications = createDataLoadingThunk(
'notifications/clear',
() => apiClearNotifications(),
);
export const markNotificationsAsRead = createAction(
'notificationGroups/markAsRead',
);
export const mountNotifications = createAction('notificationGroups/mount');
export const unmountNotifications = createAction('notificationGroups/unmount');

View file

@ -33,7 +33,6 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
@ -186,7 +185,7 @@ const noOp = () => {};
let expandNotificationsController = new AbortController();
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
export function expandNotifications({ maxId, forceLoad = false } = {}, done = noOp) {
return (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const notifications = getState().get('notifications');
@ -274,16 +273,6 @@ export function expandNotificationsFail(error, isLoadingMore) {
};
}
export function clearNotifications() {
return (dispatch) => {
dispatch({
type: NOTIFICATIONS_CLEAR,
});
api().post('/api/v1/notifications/clear');
};
}
export function scrollTopNotifications(top) {
return {
type: NOTIFICATIONS_SCROLL_TOP,

View file

@ -0,0 +1,18 @@
import { createAppAsyncThunk } from 'mastodon/store';
import { fetchNotifications } from './notification_groups';
import { expandNotifications } from './notifications';
export const initializeNotifications = createAppAsyncThunk(
'notifications/initialize',
(_, { dispatch, getState }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const enableBeta = getState().settings.getIn(
['notifications', 'groupingBeta'],
false,
) as boolean;
if (enableBeta) void dispatch(fetchNotifications());
else dispatch(expandNotifications());
},
);

View file

@ -1,11 +1,6 @@
import { createAction } from '@reduxjs/toolkit';
import type { ApiAccountJSON } from '../api_types/accounts';
// To be replaced once ApiNotificationJSON type exists
interface FakeApiNotificationJSON {
type: string;
account: ApiAccountJSON;
}
import type { ApiNotificationJSON } from 'mastodon/api_types/notifications';
export const notificationsUpdate = createAction(
'notifications/update',
@ -13,7 +8,7 @@ export const notificationsUpdate = createAction(
playSound,
...args
}: {
notification: FakeApiNotificationJSON;
notification: ApiNotificationJSON;
usePendingItems: boolean;
playSound: boolean;
}) => ({

View file

@ -1,3 +1,5 @@
import { browserHistory } from 'mastodon/components/router';
import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
@ -95,7 +97,7 @@ export function redraft(status, raw_text) {
};
}
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
export const editStatus = (id) => (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);
if (status.get('poll')) {
@ -106,7 +108,7 @@ export const editStatus = (id, routerHistory) => (dispatch, getState) => {
api().get(`/api/v1/statuses/${id}/source`).then(response => {
dispatch(fetchStatusSourceSuccess());
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
}).catch(error => {
dispatch(fetchStatusSourceFail(error));
@ -126,7 +128,7 @@ export const fetchStatusSourceFail = error => ({
error,
});
export function deleteStatus(id, routerHistory, withRedraft = false) {
export function deleteStatus(id, withRedraft = false) {
return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);
@ -143,7 +145,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
if (withRedraft) {
dispatch(redraft(status, response.data.text));
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
}
}).catch(error => {
dispatch(deleteStatusFail(id, error));
@ -311,6 +313,21 @@ export function revealStatus(ids) {
};
}
export function toggleStatusSpoilers(statusId) {
return (dispatch, getState) => {
const status = getState().statuses.get(statusId);
if (!status)
return;
if (status.get('hidden')) {
dispatch(revealStatus(statusId));
} else {
dispatch(hideStatus(statusId));
}
};
}
export function toggleStatusCollapse(id, isCollapsed) {
return {
type: STATUS_COLLAPSE,
@ -356,3 +373,15 @@ export const updateEmojiReaction = (emoji_reaction) => ({
type: STATUS_EMOJI_REACTION_UPDATE,
emoji_reaction,
});
export const navigateToStatus = (statusId) => {
return (_dispatch, getState) => {
const state = getState();
const accountId = state.statuses.getIn([statusId, 'account']);
const acct = state.accounts.getIn([accountId, 'acct']);
if (acct) {
browserHistory.push(`/@${acct}/${statusId}`);
}
};
};

View file

@ -10,6 +10,7 @@ import {
deleteAnnouncement,
} from './announcements';
import { updateConversations } from './conversations';
import { processNewNotificationForGroups } from './notification_groups';
import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications';
import { updateStatus } from './statuses';
import {
@ -99,10 +100,16 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
case 'notification': {
// @ts-expect-error
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
const notificationJSON = JSON.parse(data.payload);
dispatch(updateNotifications(notificationJSON, messages, locale));
// TODO: remove this once the groups feature replaces the previous one
if(getState().notificationGroups.groups.length > 0) {
dispatch(processNewNotificationForGroups(notificationJSON));
}
break;
}
case 'emoji_reaction':
// @ts-expect-error
dispatch(updateEmojiReactions(JSON.parse(data.payload)));

View file

@ -0,0 +1,33 @@
import api, { apiRequest, getLinks } from 'mastodon/api';
import type { ApiNotificationGroupJSON } from 'mastodon/api_types/notifications';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
const exceptInvalidNotifications = (
notifications: ApiNotificationGroupJSON[],
) => {
return notifications.filter((n) => {
if ('status' in n) {
return (n.status as ApiStatusJSON | null) !== null;
}
return true;
});
};
export const apiFetchNotifications = async (params?: {
exclude_types?: string[];
max_id?: string;
}) => {
const response = await api().request<ApiNotificationGroupJSON[]>({
method: 'GET',
url: '/api/v2_alpha/notifications',
params,
});
return {
notifications: exceptInvalidNotifications(response.data),
links: getLinks(response),
};
};
export const apiClearNotifications = () =>
apiRequest<undefined>('POST', 'v1/notifications/clear');

View file

@ -0,0 +1,149 @@
// See app/serializers/rest/notification_group_serializer.rb
import type { AccountWarningAction } from 'mastodon/models/notification_group';
import type { ApiAccountJSON } from './accounts';
import type { ApiReportJSON } from './reports';
import type { ApiStatusJSON } from './statuses';
// See app/model/notification.rb
export const allNotificationTypes = [
'follow',
'follow_request',
'favourite',
'emoji_reaction',
'reblog',
'mention',
'status_reference',
'poll',
'status',
'update',
'admin.sign_up',
'admin.report',
'moderation_warning',
'severed_relationships',
];
export type NotificationWithStatusType =
| 'favourite'
| 'emoji_reaction'
| 'reblog'
| 'status'
| 'mention'
| 'status_reference'
| 'poll'
| 'update';
export type NotificationType =
| NotificationWithStatusType
| 'follow'
| 'follow_request'
| 'moderation_warning'
| 'severed_relationships'
| 'admin.sign_up'
| 'admin.report';
export interface BaseNotificationJSON {
id: string;
type: NotificationType;
created_at: string;
group_key: string;
account: ApiAccountJSON;
}
export interface BaseNotificationGroupJSON {
group_key: string;
notifications_count: number;
type: NotificationType;
sample_accounts: ApiAccountJSON[];
latest_page_notification_at: string; // FIXME: This will only be present if the notification group is returned in a paginated list, not requested directly
most_recent_notification_id: string;
page_min_id?: string;
page_max_id?: string;
}
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
type: NotificationWithStatusType;
status: ApiStatusJSON;
}
interface NotificationWithStatusJSON extends BaseNotificationJSON {
type: NotificationWithStatusType;
status: ApiStatusJSON;
}
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'admin.report';
report: ApiReportJSON;
}
interface ReportNotificationJSON extends BaseNotificationJSON {
type: 'admin.report';
report: ApiReportJSON;
}
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
type: SimpleNotificationTypes;
}
interface SimpleNotificationJSON extends BaseNotificationJSON {
type: SimpleNotificationTypes;
}
export interface ApiAccountWarningJSON {
id: string;
action: AccountWarningAction;
text: string;
status_ids: string[];
created_at: string;
target_account: ApiAccountJSON;
appeal: unknown;
}
interface ModerationWarningNotificationGroupJSON
extends BaseNotificationGroupJSON {
type: 'moderation_warning';
moderation_warning: ApiAccountWarningJSON;
}
interface ModerationWarningNotificationJSON extends BaseNotificationJSON {
type: 'moderation_warning';
moderation_warning: ApiAccountWarningJSON;
}
export interface ApiAccountRelationshipSeveranceEventJSON {
id: string;
type: 'account_suspension' | 'domain_block' | 'user_domain_block';
purged: boolean;
target_name: string;
followers_count: number;
following_count: number;
created_at: string;
}
interface AccountRelationshipSeveranceNotificationGroupJSON
extends BaseNotificationGroupJSON {
type: 'severed_relationships';
event: ApiAccountRelationshipSeveranceEventJSON;
}
interface AccountRelationshipSeveranceNotificationJSON
extends BaseNotificationJSON {
type: 'severed_relationships';
event: ApiAccountRelationshipSeveranceEventJSON;
}
export type ApiNotificationJSON =
| SimpleNotificationJSON
| ReportNotificationJSON
| AccountRelationshipSeveranceNotificationJSON
| NotificationWithStatusJSON
| ModerationWarningNotificationJSON;
export type ApiNotificationGroupJSON =
| SimpleNotificationGroupJSON
| ReportNotificationGroupJSON
| AccountRelationshipSeveranceNotificationGroupJSON
| NotificationGroupWithStatusJSON
| ModerationWarningNotificationGroupJSON;

View file

@ -0,0 +1,16 @@
import type { ApiAccountJSON } from './accounts';
export type ReportCategory = 'other' | 'spam' | 'legal' | 'violation';
export interface ApiReportJSON {
id: string;
action_taken: unknown;
action_taken_at: unknown;
category: ReportCategory;
comment: string;
forwarded: boolean;
created_at: string;
status_ids: string[];
rule_ids: string[];
target_account: ApiAccountJSON;
}

View file

@ -165,7 +165,7 @@ describe('computeHashtagBarForStatus', () => {
);
});
it('puts the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
it('does not put the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
const status = createStatus(
'<p>This is my content! <a href="test">#hashtag</a></p>',
['hashtag'],

View file

@ -9,18 +9,18 @@ const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
});
interface Props {
interface Props<T> {
disabled: boolean;
maxId: string;
onClick: (maxId: string) => void;
param: T;
onClick: (params: T) => void;
}
export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
export const LoadGap = <T,>({ disabled, param, onClick }: Props<T>) => {
const intl = useIntl();
const handleClick = useCallback(() => {
onClick(maxId);
}, [maxId, onClick]);
onClick(param);
}, [param, onClick]);
return (
<button

View file

@ -22,7 +22,7 @@ type LocationState = MastodonLocationState | null | undefined;
type HistoryPath = Path | LocationDescriptor<LocationState>;
const browserHistory = createBrowserHistory<LocationState>();
export const browserHistory = createBrowserHistory<LocationState>();
const originalPush = browserHistory.push.bind(browserHistory);
const originalReplace = browserHistory.replace.bind(browserHistory);

View file

@ -124,7 +124,10 @@ class Status extends ImmutablePureComponent {
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
scrollKey: PropTypes.string,
skipPrepend: PropTypes.bool,
avatarSize: PropTypes.number,
deployPictureInPicture: PropTypes.func,
unfocusable: PropTypes.bool,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
@ -276,7 +279,7 @@ class Status extends ImmutablePureComponent {
handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this._properStatus(), this.props.history);
this.props.onReply(this._properStatus());
};
handleHotkeyFavourite = () => {
@ -289,7 +292,7 @@ class Status extends ImmutablePureComponent {
handleHotkeyMention = e => {
e.preventDefault();
this.props.onMention(this._properStatus().get('account'), this.props.history);
this.props.onMention(this._properStatus().get('account'));
};
handleHotkeyOpen = () => {
@ -363,7 +366,7 @@ class Status extends ImmutablePureComponent {
};
render () {
const { intl, hidden, featured, unread, muted, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, withoutQuote } = this.props;
const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, withoutQuote, skipPrepend, avatarSize = 46 } = this.props;
let { status, account, ...other } = this.props;
@ -373,7 +376,7 @@ class Status extends ImmutablePureComponent {
return null;
}
const handlers = muted ? {} : {
const handlers = this.props.muted ? {} : {
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
@ -391,8 +394,8 @@ class Status extends ImmutablePureComponent {
if (hidden) {
return (
<HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !muted })} tabIndex={0}>
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={unfocusable ? null : 0}>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span>
</div>
@ -407,6 +410,25 @@ class Status extends ImmutablePureComponent {
let visibilityName = status.get('limited_scope') || status.get('visibility_ex') || status.get('visibility');
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
return (
<HotKeys handlers={minHandlers} tabIndex={unfocusable ? null : -1}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={unfocusable ? null : 0} ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
</button>
</div>
</HotKeys>
);
}
if (featured) {
prepend = (
<div className='status__prepend'>
@ -454,7 +476,7 @@ class Status extends ImmutablePureComponent {
}
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = muted ? {} : {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
@ -511,7 +533,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language');
if (muted) {
if (this.props.muted) {
media = (
<AttachmentList
compact
@ -589,7 +611,7 @@ class Status extends ImmutablePureComponent {
</Bundle>
);
}
} else if (status.get('card') && !muted) {
} else if (status.get('card') && !this.props.muted) {
media = (
<Card
onOpenMedia={this.handleOpenMedia}
@ -603,6 +625,7 @@ class Status extends ImmutablePureComponent {
visibilityName = status.get('limited_scope') || status.get('visibility_ex') || status.get('visibility');
let emojiReactionsBar = null;
if (!this.props.withoutEmojiReactions && status.get('emoji_reactions')) {
const emojiReactions = status.get('emoji_reactions');
@ -612,6 +635,12 @@ class Status extends ImmutablePureComponent {
}
}
if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={avatarSize} />;
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
@ -620,14 +649,14 @@ class Status extends ImmutablePureComponent {
const withReference = (!withQuote && status.get('status_references_count') > 0) ? <span className='status__visibility-icon'><Icon id='link' icon={ReferenceIcon} title='Quiet quote' /></span> : null;
const withExpiration = status.get('expires_at') ? <span className='status__visibility-icon'><Icon id='clock-o' icon={TimerIcon} title='Expiration' /></span> : null;
const quote = !muted && !withoutQuote && status.get('quote_id') && (['public', 'community'].includes(contextType) ? isShowItem('quote_in_public') : isShowItem('quote_in_home')) && <CompactedStatusContainer id={status.get('quote_id')} history={this.props.history} />;
const quote = !this.props.muted && !withoutQuote && status.get('quote_id') && (['public', 'community'].includes(contextType) ? isShowItem('quote_in_public') : isShowItem('quote_in_home')) && <CompactedStatusContainer id={status.get('quote_id')} history={this.props.history} />;
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility_ex')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !muted })} tabIndex={muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
{prepend}
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility_ex')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted || unfocusable ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
{!skipPrepend && prepend}
<div className={classNames('status', `status-${status.get('visibility_ex')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: muted })} data-id={status.get('id')}>
<div className={classNames('status', `status-${status.get('visibility_ex')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}

View file

@ -134,7 +134,7 @@ class StatusActionBar extends ImmutablePureComponent {
const { signedIn } = this.props.identity;
if (signedIn) {
this.props.onReply(this.props.status, this.props.history);
this.props.onReply(this.props.status);
} else {
this.props.onInteractionModal('reply', this.props.status);
}
@ -201,15 +201,15 @@ class StatusActionBar extends ImmutablePureComponent {
};
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.props.history);
this.props.onDelete(this.props.status);
};
handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.props.history, true);
this.props.onDelete(this.props.status, true);
};
handleEditClick = () => {
this.props.onEdit(this.props.status, this.props.history);
this.props.onEdit(this.props.status);
};
handlePinClick = () => {
@ -217,11 +217,11 @@ class StatusActionBar extends ImmutablePureComponent {
};
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.props.history);
this.props.onMention(this.props.status.get('account'));
};
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.props.history);
this.props.onDirect(this.props.status.get('account'));
};
handleMuteClick = () => {

View file

@ -107,7 +107,7 @@ export default class StatusList extends ImmutablePureComponent {
<LoadGap
key={'gap:' + statusIds.get(index + 1)}
disabled={isLoading}
maxId={index > 0 ? statusIds.get(index - 1) : null}
param={index > 0 ? statusIds.get(index - 1) : null}
onClick={onLoadMore}
/>
);

View file

@ -22,13 +22,11 @@ import {
initAddFilter,
} from '../actions/filters';
import {
reblog,
favourite,
emojiReact,
bookmark,
unreblog,
unfavourite,
unEmojiReact,
toggleReblog,
toggleFavourite,
bookmark,
unbookmark,
pin,
unpin,
@ -41,15 +39,14 @@ import {
muteStatus,
unmuteStatus,
deleteStatus,
hideStatus,
revealStatus,
toggleStatusSpoilers,
toggleStatusCollapse,
editStatus,
translateStatus,
undoStatusTranslation,
} from '../actions/statuses';
import Status from '../components/status';
import { boostModal, deleteModal } from '../initial_state';
import { deleteModal } from '../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
const messages = defineMessages({
@ -81,7 +78,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
contextType,
onReply (status, router) {
onReply (status) {
dispatch((_, getState) => {
let state = getState();
@ -91,28 +88,16 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)) },
onConfirm: () => dispatch(replyCompose(status)) },
}));
} else {
dispatch(replyCompose(status, router));
dispatch(replyCompose(status));
}
});
},
onModalReblog (status, privacy) {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
}
},
onReblog (status, e) {
if ((e && e.shiftKey) || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
}
dispatch(toggleReblog(status.get('id'), e.shiftKey));
},
onReblogForceModal (status) {
@ -120,11 +105,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
dispatch(toggleFavourite(status.get('id')));
},
onEmojiReact (status, emoji) {
@ -170,22 +151,22 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}));
},
onDelete (status, history, withRedraft = false) {
onDelete (status, withRedraft = false) {
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
dispatch(deleteStatus(status.get('id'), withRedraft));
} else {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
},
}));
}
},
onEdit (status, history) {
onEdit (status) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
@ -194,11 +175,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
modalProps: {
message: intl.formatMessage(messages.editMessage),
confirm: intl.formatMessage(messages.editConfirm),
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
onConfirm: () => dispatch(editStatus(status.get('id'))),
},
}));
} else {
dispatch(editStatus(status.get('id'), history));
dispatch(editStatus(status.get('id')));
}
});
},
@ -219,12 +200,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}
},
onDirect (account, router) {
dispatch(directCompose(account, router));
onDirect (account) {
dispatch(directCompose(account));
},
onMention (account, router) {
dispatch(mentionCompose(account, router));
onMention (account) {
dispatch(mentionCompose(account));
},
onOpenMedia (statusId, media, index, lang) {
@ -275,11 +256,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')));
} else {
dispatch(hideStatus(status.get('id')));
}
dispatch(toggleStatusSpoilers(status.get('id')));
},
onToggleCollapsed (status, isCollapsed) {

View file

@ -2,13 +2,11 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { NavLink, withRouter } from 'react-router-dom';
import { NavLink } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import InnerHeader from '../../account/components/header';
import MemorialNote from './memorial_note';
@ -40,7 +38,6 @@ class Header extends ImmutablePureComponent {
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
...WithRouterPropTypes,
};
handleFollow = () => {
@ -52,11 +49,11 @@ class Header extends ImmutablePureComponent {
};
handleMention = () => {
this.props.onMention(this.props.account, this.props.history);
this.props.onMention(this.props.account);
};
handleDirect = () => {
this.props.onDirect(this.props.account, this.props.history);
this.props.onDirect(this.props.account);
};
handleReport = () => {
@ -175,4 +172,4 @@ class Header extends ImmutablePureComponent {
}
export default withRouter(Header);
export default Header;

View file

@ -78,12 +78,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onMention (account, router) {
dispatch(mentionCompose(account, router));
onMention (account) {
dispatch(mentionCompose(account));
},
onDirect (account, router) {
dispatch(directCompose(account, router));
onDirect (account) {
dispatch(directCompose(account));
},
onReblogToggle (account) {

View file

@ -10,9 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
import AutosuggestInput from '../../../components/autosuggest_input';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import { Button } from '../../../components/button';
@ -81,7 +78,6 @@ class ComposeForm extends ImmutablePureComponent {
lang: PropTypes.string,
circleId: PropTypes.string,
maxChars: PropTypes.number,
...WithOptionalRouterPropTypes
};
static defaultProps = {
@ -130,7 +126,7 @@ class ComposeForm extends ImmutablePureComponent {
return;
}
this.props.onSubmit(this.props.history || null);
this.props.onSubmit();
if (e) {
e.preventDefault();
@ -349,4 +345,4 @@ class ComposeForm extends ImmutablePureComponent {
}
export default withOptionalRouter(injectIntl(ComposeForm));
export default injectIntl(ComposeForm);

View file

@ -13,6 +13,7 @@ import { cancelReplyCompose } from 'mastodon/actions/compose';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { EmbeddedStatusContent } from 'mastodon/features/notifications_v2/components/embedded_status_content';
const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
@ -33,8 +34,6 @@ export const EditIndicator = () => {
return null;
}
const content = { __html: status.get('contentHtml') };
return (
<div className='edit-indicator'>
<div className='edit-indicator__header'>
@ -49,7 +48,12 @@ export const EditIndicator = () => {
</div>
</div>
<div className='edit-indicator__content translate' dangerouslySetInnerHTML={content} />
<EmbeddedStatusContent
className='edit-indicator__content translate'
content={status.get('contentHtml')}
language={status.get('language')}
mentions={status.get('mentions')}
/>
{(status.get('poll') || status.get('media_attachments').size > 0) && (
<div className='edit-indicator__attachments'>

View file

@ -9,6 +9,7 @@ import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react'
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon';
import { EmbeddedStatusContent } from 'mastodon/features/notifications_v2/components/embedded_status_content';
export const ReplyIndicator = () => {
const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
@ -19,8 +20,6 @@ export const ReplyIndicator = () => {
return null;
}
const content = { __html: status.get('contentHtml') };
return (
<div className='reply-indicator'>
<div className='reply-indicator__line' />
@ -34,7 +33,12 @@ export const ReplyIndicator = () => {
<DisplayName account={account} />
</Link>
<div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
<EmbeddedStatusContent
className='reply-indicator__content translate'
content={status.get('contentHtml')}
language={status.get('language')}
mentions={status.get('mentions')}
/>
{(status.get('poll') || status.get('media_attachments').size > 0) && (
<div className='reply-indicator__attachments'>

View file

@ -41,8 +41,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(changeCompose(text));
},
onSubmit (router) {
dispatch(submitCompose(router));
onSubmit () {
dispatch(submitCompose());
},
onClearSuggestions () {

View file

@ -18,7 +18,7 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import { replyCompose } from 'mastodon/actions/compose';
import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
import { openModal } from 'mastodon/actions/modal';
import { muteStatus, unmuteStatus, revealStatus, hideStatus } from 'mastodon/actions/statuses';
import { muteStatus, unmuteStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
import AttachmentList from 'mastodon/components/attachment_list';
import AvatarComposite from 'mastodon/components/avatar_composite';
import { IconButton } from 'mastodon/components/icon_button';
@ -108,14 +108,14 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(lastStatus, history)),
onConfirm: () => dispatch(replyCompose(lastStatus)),
},
}));
} else {
dispatch(replyCompose(lastStatus, history));
dispatch(replyCompose(lastStatus));
}
});
}, [dispatch, lastStatus, history, intl]);
}, [dispatch, lastStatus, intl]);
const handleDelete = useCallback(() => {
dispatch(deleteConversation(id));
@ -138,11 +138,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
}, [dispatch, lastStatus]);
const handleShowMore = useCallback(() => {
if (lastStatus.get('hidden')) {
dispatch(revealStatus(lastStatus.get('id')));
} else {
dispatch(hideStatus(lastStatus.get('id')));
}
dispatch(toggleStatusSpoilers(lastStatus.get('id')));
}, [dispatch, lastStatus]);
if (!lastStatus) {

View file

@ -59,6 +59,7 @@ class ColumnSettings extends PureComponent {
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
const groupingShowStr = <FormattedMessage id='notifications.column_settings.beta.grouping' defaultMessage='Group notifications' />;
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
@ -110,6 +111,16 @@ class ColumnSettings extends PureComponent {
</div>
</section>
<section role='group' aria-labelledby='notifications-beta'>
<h3 id='notifications-beta'>
<FormattedMessage id='notifications.column_settings.beta.category' defaultMessage='Experimental features' />
</h3>
<div className='column-settings__row'>
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['groupingBeta']} onChange={onChange} label={groupingShowStr} />
</div>
</section>
<section role='group' aria-labelledby='notifications-unread-markers'>
<h3 id='notifications-unread-markers'>
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />

View file

@ -35,7 +35,9 @@ export const FilteredNotificationsBanner: React.FC = () => {
className='filtered-notifications-banner'
to='/notifications/requests'
>
<Icon icon={InventoryIcon} id='filtered-notifications' />
<div className='notification-group__icon'>
<Icon icon={InventoryIcon} id='filtered-notifications' />
</div>
<div className='filtered-notifications-banner__text'>
<strong>

View file

@ -1,7 +1,10 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import GavelIcon from '@/material-icons/400-24px/gavel.svg?react';
import { Icon } from 'mastodon/components/icon';
import type { AccountWarningAction } from 'mastodon/models/notification_group';
// This needs to be kept in sync with app/models/account_warning.rb
const messages = defineMessages({
@ -40,20 +43,18 @@ const messages = defineMessages({
});
interface Props {
action:
| 'none'
| 'disable'
| 'force_cw'
| 'mark_statuses_as_sensitive'
| 'delete_statuses'
| 'sensitive'
| 'silence'
| 'suspend';
action: AccountWarningAction;
id: string;
hidden: boolean;
hidden?: boolean;
unread?: boolean;
}
export const ModerationWarning: React.FC<Props> = ({ action, id, hidden }) => {
export const ModerationWarning: React.FC<Props> = ({
action,
id,
hidden,
unread,
}) => {
const intl = useIntl();
if (hidden) {
@ -61,23 +62,32 @@ export const ModerationWarning: React.FC<Props> = ({ action, id, hidden }) => {
}
return (
<a
href={`/disputes/strikes/${id}`}
target='_blank'
rel='noopener noreferrer'
className='notification__moderation-warning'
<div
role='button'
className={classNames(
'notification-group notification-group--link notification-group--moderation-warning focusable',
{ 'notification-group--unread': unread },
)}
tabIndex={0}
>
<Icon id='warning' icon={GavelIcon} />
<div className='notification-group__icon'>
<Icon id='warning' icon={GavelIcon} />
</div>
<div className='notification__moderation-warning__content'>
<div className='notification-group__main'>
<p>{intl.formatMessage(messages[action])}</p>
<span className='link-button'>
<a
href={`/disputes/strikes/${id}`}
target='_blank'
rel='noopener noreferrer'
className='link-button'
>
<FormattedMessage
id='notification.moderation-warning.learn_more'
defaultMessage='Learn more'
/>
</span>
</a>
</div>
</a>
</div>
);
};

View file

@ -38,7 +38,7 @@ const messages = defineMessages({
emojiReaction: { id: 'notification.emoji_reaction', defaultMessage: '{name} reacted your status with emoji' },
follow: { id: 'notification.follow', defaultMessage: '{name} followed you' },
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
poll: { id: 'notification.poll', defaultMessage: 'A poll you voted in has ended' },
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
listStatus: { id: 'notification.list_status', defaultMessage: '{name} post is added to {listName}' },
@ -107,7 +107,7 @@ class Notification extends ImmutablePureComponent {
e.preventDefault();
const { notification, onMention } = this.props;
onMention(notification.get('account'), this.props.history);
onMention(notification.get('account'));
};
handleHotkeyFavourite = () => {
@ -452,7 +452,7 @@ class Notification extends ImmutablePureComponent {
{ownPoll ? (
<FormattedMessage id='notification.own_poll' defaultMessage='Your poll has ended' />
) : (
<FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' />
<FormattedMessage id='notification.poll' defaultMessage='A poll you voted in has ended' />
)}
</span>
</div>

View file

@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
import { Icon } from 'mastodon/components/icon';
import { domain } from 'mastodon/initial_state';
@ -13,7 +15,7 @@ const messages = defineMessages({
user_domain_block: { id: 'notification.relationships_severance_event.user_domain_block', defaultMessage: 'You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
});
export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden }) => {
export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden, unread }) => {
const intl = useIntl();
if (hidden) {
@ -21,14 +23,14 @@ export const RelationshipsSeveranceEvent = ({ type, target, followingCount, foll
}
return (
<a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='notification__relationships-severance-event'>
<Icon id='heart_broken' icon={HeartBrokenIcon} />
<div role='button' className={classNames('notification-group notification-group--link notification-group--relationships-severance-event focusable', { 'notification-group--unread': unread })} tabIndex='0'>
<div className='notification-group__icon'><Icon id='heart_broken' icon={HeartBrokenIcon} /></div>
<div className='notification__relationships-severance-event__content'>
<div className='notification-group__main'>
<p>{intl.formatMessage(messages[type], { from: <strong>{domain}</strong>, target: <strong>{target}</strong>, followingCount, followersCount })}</p>
<span className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></span>
<a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></a>
</div>
</a>
</div>
);
};
@ -42,4 +44,5 @@ RelationshipsSeveranceEvent.propTypes = {
followersCount: PropTypes.number.isRequired,
followingCount: PropTypes.number.isRequired,
hidden: PropTypes.bool,
unread: PropTypes.bool,
};

View file

@ -2,10 +2,13 @@ import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { initializeNotifications } from 'mastodon/actions/notifications_migration';
import { showAlert } from '../../../actions/alerts';
import { openModal } from '../../../actions/modal';
import { clearNotifications } from '../../../actions/notification_groups';
import { updateNotificationsPolicy } from '../../../actions/notification_policies';
import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications';
import { setFilter, requestBrowserPermission } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
import { changeSetting } from '../../../actions/settings';
import ColumnSettings from '../components/column_settings';
@ -58,6 +61,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} else {
dispatch(changeSetting(['notifications', ...path], checked));
}
} else if(path[0] === 'groupingBeta') {
dispatch(changeSetting(['notifications', ...path], checked));
dispatch(initializeNotifications());
} else {
dispatch(changeSetting(['notifications', ...path], checked));
}

View file

@ -2,18 +2,13 @@ import { connect } from 'react-redux';
import { mentionCompose } from '../../../actions/compose';
import {
reblog,
favourite,
unreblog,
unfavourite,
emojiReact,
toggleFavourite,
toggleReblog,
} from '../../../actions/interactions';
import { openModal } from '../../../actions/modal';
import {
hideStatus,
revealStatus,
toggleStatusSpoilers,
} from '../../../actions/statuses';
import { boostModal } from '../../../initial_state';
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
import Notification from '../components/notification';
@ -35,40 +30,20 @@ const makeMapStateToProps = () => {
};
const mapDispatchToProps = dispatch => ({
onMention: (account, router) => {
dispatch(mentionCompose(account, router));
},
onModalReblog (status, privacy) {
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
onMention: (account) => {
dispatch(mentionCompose(account));
},
onReblog (status, e) {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
if (e.shiftKey || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
}
}
dispatch(toggleReblog(status.get('id'), e.shiftKey));
},
onReblogForceModal (status) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
}
dispatch(toggleReblog(status.get('id'), true, true));
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
dispatch(toggleFavourite(status.get('id')));
},
onEmojiReact (status, emoji) {
@ -76,11 +51,7 @@ const mapDispatchToProps = dispatch => ({
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')));
} else {
dispatch(hideStatus(status.get('id')));
}
dispatch(toggleStatusSpoilers(status.get('id')));
},
});

View file

@ -202,7 +202,7 @@ class Notifications extends PureComponent {
<LoadGap
key={'gap:' + notifications.getIn([index + 1, 'id'])}
disabled={isLoading}
maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
param={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
onClick={this.handleLoadGap}
/>
) : (

View file

@ -0,0 +1,31 @@
import { Link } from 'react-router-dom';
import { Avatar } from 'mastodon/components/avatar';
import { NOTIFICATIONS_GROUP_MAX_AVATARS } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store';
const AvatarWrapper: React.FC<{ accountId: string }> = ({ accountId }) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
if (!account) return null;
return (
<Link
to={`/@${account.acct}`}
title={`@${account.acct}`}
data-hover-card-account={account.id}
>
<Avatar account={account} size={28} />
</Link>
);
};
export const AvatarGroup: React.FC<{ accountIds: string[] }> = ({
accountIds,
}) => (
<div className='notification-group__avatar-group'>
{accountIds.slice(0, NOTIFICATIONS_GROUP_MAX_AVATARS).map((accountId) => (
<AvatarWrapper key={accountId} accountId={accountId} />
))}
</div>
);

View file

@ -0,0 +1,159 @@
import { useCallback, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import type { List as ImmutableList, RecordOf } from 'immutable';
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon';
import type { Status } from 'mastodon/models/status';
import { useAppSelector } from 'mastodon/store';
import { EmbeddedStatusContent } from './embedded_status_content';
export type Mention = RecordOf<{ url: string; acct: string }>;
export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
statusId,
}) => {
const history = useHistory();
const clickCoordinatesRef = useRef<[number, number] | null>();
const status = useAppSelector(
(state) => state.statuses.get(statusId) as Status | undefined,
);
const account = useAppSelector((state) =>
state.accounts.get(status?.get('account') as string),
);
const handleMouseDown = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ clientX, clientY }) => {
clickCoordinatesRef.current = [clientX, clientY];
},
[clickCoordinatesRef],
);
const handleMouseUp = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ clientX, clientY, target, button }) => {
const [startX, startY] = clickCoordinatesRef.current ?? [0, 0];
const [deltaX, deltaY] = [
Math.abs(clientX - startX),
Math.abs(clientY - startY),
];
let element: HTMLDivElement | null = target as HTMLDivElement;
while (element) {
if (
element.localName === 'button' ||
element.localName === 'a' ||
element.localName === 'label'
) {
return;
}
element = element.parentNode as HTMLDivElement | null;
}
if (deltaX + deltaY < 5 && button === 0 && account) {
history.push(`/@${account.acct}/${statusId}`);
}
clickCoordinatesRef.current = null;
},
[clickCoordinatesRef, statusId, account, history],
);
const handleMouseEnter = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ currentTarget }) => {
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
for (const emoji of emojis) {
const newSrc = emoji.getAttribute('data-original');
if (newSrc) emoji.src = newSrc;
}
},
[],
);
const handleMouseLeave = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ currentTarget }) => {
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
for (const emoji of emojis) {
const newSrc = emoji.getAttribute('data-static');
if (newSrc) emoji.src = newSrc;
}
},
[],
);
if (!status) {
return null;
}
// Assign status attributes to variables with a forced type, as status is not yet properly typed
const contentHtml = status.get('contentHtml') as string;
const poll = status.get('poll');
const language = status.get('language') as string;
const mentions = status.get('mentions') as ImmutableList<Mention>;
const mediaAttachmentsSize = (
status.get('media_attachments') as ImmutableList<unknown>
).size;
return (
<div
className='notification-group__embedded-status'
role='button'
tabIndex={-1}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className='notification-group__embedded-status__account'>
<Avatar account={account} size={16} />
<DisplayName account={account} />
</div>
<EmbeddedStatusContent
className='notification-group__embedded-status__content reply-indicator__content translate'
content={contentHtml}
language={language}
mentions={mentions}
/>
{(poll || mediaAttachmentsSize > 0) && (
<div className='notification-group__embedded-status__attachments reply-indicator__attachments'>
{!!poll && (
<>
<Icon icon={BarChart4BarsIcon} id='bar-chart-4-bars' />
<FormattedMessage
id='reply_indicator.poll'
defaultMessage='Poll'
/>
</>
)}
{mediaAttachmentsSize > 0 && (
<>
<Icon icon={PhotoLibraryIcon} id='photo-library' />
<FormattedMessage
id='reply_indicator.attachments'
defaultMessage='{count, plural, one {# attachment} other {# attachments}}'
values={{ count: mediaAttachmentsSize }}
/>
</>
)}
</div>
)}
</div>
);
};

View file

@ -0,0 +1,93 @@
import { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import type { List } from 'immutable';
import type { History } from 'history';
import type { Mention } from './embedded_status';
const handleMentionClick = (
history: History,
mention: Mention,
e: MouseEvent,
) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
history.push(`/@${mention.get('acct')}`);
}
};
const handleHashtagClick = (
history: History,
hashtag: string,
e: MouseEvent,
) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
history.push(`/tags/${hashtag.replace(/^#/, '')}`);
}
};
export const EmbeddedStatusContent: React.FC<{
content: string;
mentions: List<Mention>;
language: string;
className?: string;
}> = ({ content, mentions, language, className }) => {
const history = useHistory();
const handleContentRef = useCallback(
(node: HTMLDivElement | null) => {
if (!node) {
return;
}
const links = node.querySelectorAll<HTMLAnchorElement>('a');
for (const link of links) {
if (link.classList.contains('status-link')) {
continue;
}
link.classList.add('status-link');
const mention = mentions.find((item) => link.href === item.get('url'));
if (mention) {
link.addEventListener(
'click',
handleMentionClick.bind(null, history, mention),
false,
);
link.setAttribute('title', `@${mention.get('acct')}`);
link.setAttribute('href', `/@${mention.get('acct')}`);
} else if (
link.textContent?.[0] === '#' ||
link.previousSibling?.textContent?.endsWith('#')
) {
link.addEventListener(
'click',
handleHashtagClick.bind(null, history, link.text),
false,
);
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
} else {
link.setAttribute('title', link.href);
link.classList.add('unhandled-link');
}
}
},
[mentions, history],
);
return (
<div
className={className}
ref={handleContentRef}
lang={language}
dangerouslySetInnerHTML={{ __html: content }}
/>
);
};

View file

@ -0,0 +1,51 @@
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { useAppSelector } from 'mastodon/store';
export const NamesList: React.FC<{
accountIds: string[];
total: number;
seeMoreHref?: string;
}> = ({ accountIds, total, seeMoreHref }) => {
const lastAccountId = accountIds[0] ?? '0';
const account = useAppSelector((state) => state.accounts.get(lastAccountId));
if (!account) return null;
const displayedName = (
<Link
to={`/@${account.acct}`}
title={`@${account.acct}`}
data-hover-card-account={account.id}
>
<bdi dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
</Link>
);
if (total === 1) {
return displayedName;
}
if (seeMoreHref)
return (
<FormattedMessage
id='name_and_others_with_link'
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a>'
values={{
name: displayedName,
count: total - 1,
a: (chunks) => <Link to={seeMoreHref}>{chunks}</Link>,
}}
/>
);
return (
<FormattedMessage
id='name_and_others'
defaultMessage='{name} and {count, plural, one {# other} other {# others}}'
values={{ name: displayedName, count: total - 1 }}
/>
);
};

View file

@ -0,0 +1,132 @@
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import classNames from 'classnames';
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import type { NotificationGroupAdminReport } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store';
// This needs to be kept in sync with app/models/report.rb
const messages = defineMessages({
other: {
id: 'report_notification.categories.other_sentence',
defaultMessage: 'other',
},
spam: {
id: 'report_notification.categories.spam_sentence',
defaultMessage: 'spam',
},
legal: {
id: 'report_notification.categories.legal_sentence',
defaultMessage: 'illegal content',
},
violation: {
id: 'report_notification.categories.violation_sentence',
defaultMessage: 'rule violation',
},
});
export const NotificationAdminReport: React.FC<{
notification: NotificationGroupAdminReport;
unread?: boolean;
}> = ({ notification, notification: { report }, unread }) => {
const intl = useIntl();
const targetAccount = useAppSelector((state) =>
state.accounts.get(report.targetAccountId),
);
const account = useAppSelector((state) =>
state.accounts.get(notification.sampleAccountIds[0] ?? '0'),
);
if (!account || !targetAccount) return null;
const values = {
name: (
<bdi
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
/>
),
target: (
<bdi
dangerouslySetInnerHTML={{
__html: targetAccount.get('display_name_html'),
}}
/>
),
category: intl.formatMessage(messages[report.category]),
count: report.status_ids.length,
};
let message;
if (report.status_ids.length > 0) {
if (report.category === 'other') {
message = (
<FormattedMessage
id='notification.admin.report_account_other'
defaultMessage='{name} reported {count, plural, one {one post} other {# posts}} from {target}'
values={values}
/>
);
} else {
message = (
<FormattedMessage
id='notification.admin.report_account'
defaultMessage='{name} reported {count, plural, one {one post} other {# posts}} from {target} for {category}'
values={values}
/>
);
}
} else {
if (report.category === 'other') {
message = (
<FormattedMessage
id='notification.admin.report_statuses_other'
defaultMessage='{name} reported {target}'
values={values}
/>
);
} else {
message = (
<FormattedMessage
id='notification.admin.report_statuses'
defaultMessage='{name} reported {target} for {category}'
values={values}
/>
);
}
}
return (
<a
href={`/admin/reports/${report.id}`}
target='_blank'
rel='noopener noreferrer'
className={classNames(
'notification-group notification-group--link notification-group--admin-report focusable',
{ 'notification-group--unread': unread },
)}
>
<div className='notification-group__icon'>
<Icon id='flag' icon={FlagIcon} />
</div>
<div className='notification-group__main'>
<div className='notification-group__main__header'>
<div className='notification-group__main__header__label'>
{message}
<RelativeTimestamp timestamp={report.created_at} />
</div>
</div>
{report.comment.length > 0 && (
<div className='notification-group__embedded-status__content'>
{report.comment}
</div>
)}
</div>
</a>
);
};

View file

@ -0,0 +1,31 @@
import { FormattedMessage } from 'react-intl';
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import type { NotificationGroupAdminSignUp } from 'mastodon/models/notification_group';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.admin.sign_up'
defaultMessage='{name} signed up'
values={values}
/>
);
export const NotificationAdminSignUp: React.FC<{
notification: NotificationGroupAdminSignUp;
unread: boolean;
}> = ({ notification, unread }) => (
<NotificationGroupWithStatus
type='admin-sign-up'
icon={PersonAddIcon}
iconId='person-add'
accountIds={notification.sampleAccountIds}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}
labelRenderer={labelRenderer}
unread={unread}
/>
);

View file

@ -0,0 +1,47 @@
import { FormattedMessage } from 'react-intl';
import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react';
import type { NotificationGroupEmojiReaction } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.emoji_reaction'
defaultMessage='{name} reacted your post with emoji'
values={values}
/>
);
export const NotificationEmojiReaction: React.FC<{
notification: NotificationGroupEmojiReaction;
unread: boolean;
}> = ({ notification, unread }) => {
const { statusId } = notification;
const statusAccount = useAppSelector(
(state) =>
state.accounts.get(state.statuses.getIn([statusId, 'account']) as string)
?.acct,
);
return (
<NotificationGroupWithStatus
type='emoji_reaction'
icon={EmojiReactionIcon}
iconId='star'
accountIds={notification.sampleAccountIds}
statusId={notification.statusId}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}
labelRenderer={labelRenderer}
labelSeeMoreHref={
statusAccount
? `/@${statusAccount}/${statusId}/emoji_reactions`
: undefined
}
unread={unread}
/>
);
};

View file

@ -0,0 +1,45 @@
import { FormattedMessage } from 'react-intl';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import type { NotificationGroupFavourite } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.favourite'
defaultMessage='{name} favorited your status'
values={values}
/>
);
export const NotificationFavourite: React.FC<{
notification: NotificationGroupFavourite;
unread: boolean;
}> = ({ notification, unread }) => {
const { statusId } = notification;
const statusAccount = useAppSelector(
(state) =>
state.accounts.get(state.statuses.getIn([statusId, 'account']) as string)
?.acct,
);
return (
<NotificationGroupWithStatus
type='favourite'
icon={StarIcon}
iconId='star'
accountIds={notification.sampleAccountIds}
statusId={notification.statusId}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}
labelRenderer={labelRenderer}
labelSeeMoreHref={
statusAccount ? `/@${statusAccount}/${statusId}/favourites` : undefined
}
unread={unread}
/>
);
};

View file

@ -0,0 +1,31 @@
import { FormattedMessage } from 'react-intl';
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import type { NotificationGroupFollow } from 'mastodon/models/notification_group';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.follow'
defaultMessage='{name} followed you'
values={values}
/>
);
export const NotificationFollow: React.FC<{
notification: NotificationGroupFollow;
unread: boolean;
}> = ({ notification, unread }) => (
<NotificationGroupWithStatus
type='follow'
icon={PersonAddIcon}
iconId='person-add'
accountIds={notification.sampleAccountIds}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}
labelRenderer={labelRenderer}
unread={unread}
/>
);

View file

@ -0,0 +1,78 @@
import { useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import {
authorizeFollowRequest,
rejectFollowRequest,
} from 'mastodon/actions/accounts';
import { IconButton } from 'mastodon/components/icon_button';
import type { NotificationGroupFollowRequest } from 'mastodon/models/notification_group';
import { useAppDispatch } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status';
const messages = defineMessages({
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
});
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.follow_request'
defaultMessage='{name} has requested to follow you'
values={values}
/>
);
export const NotificationFollowRequest: React.FC<{
notification: NotificationGroupFollowRequest;
unread: boolean;
}> = ({ notification, unread }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onAuthorize = useCallback(() => {
dispatch(authorizeFollowRequest(notification.sampleAccountIds[0]));
}, [dispatch, notification.sampleAccountIds]);
const onReject = useCallback(() => {
dispatch(rejectFollowRequest(notification.sampleAccountIds[0]));
}, [dispatch, notification.sampleAccountIds]);
const actions = (
<div className='notification-group__actions'>
<IconButton
title={intl.formatMessage(messages.reject)}
icon='times'
iconComponent={CloseIcon}
onClick={onReject}
/>
<IconButton
title={intl.formatMessage(messages.authorize)}
icon='check'
iconComponent={CheckIcon}
onClick={onAuthorize}
/>
</div>
);
return (
<NotificationGroupWithStatus
type='follow-request'
icon={PersonAddIcon}
iconId='person-add'
accountIds={notification.sampleAccountIds}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}
labelRenderer={labelRenderer}
actions={actions}
unread={unread}
/>
);
};

View file

@ -0,0 +1,169 @@
import { useMemo } from 'react';
import { HotKeys } from 'react-hotkeys';
import { navigateToProfile } from 'mastodon/actions/accounts';
import { mentionComposeById } from 'mastodon/actions/compose';
import type { NotificationGroup as NotificationGroupModel } from 'mastodon/models/notification_group';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { NotificationAdminReport } from './notification_admin_report';
import { NotificationAdminSignUp } from './notification_admin_sign_up';
import { NotificationEmojiReaction } from './notification_emoji_reaction';
import { NotificationFavourite } from './notification_favourite';
import { NotificationFollow } from './notification_follow';
import { NotificationFollowRequest } from './notification_follow_request';
import { NotificationMention } from './notification_mention';
import { NotificationModerationWarning } from './notification_moderation_warning';
import { NotificationPoll } from './notification_poll';
import { NotificationReblog } from './notification_reblog';
import { NotificationSeveredRelationships } from './notification_severed_relationships';
import { NotificationStatus } from './notification_status';
import { NotificationStatusReference } from './notification_status_reference';
import { NotificationUpdate } from './notification_update';
export const NotificationGroup: React.FC<{
notificationGroupId: NotificationGroupModel['group_key'];
unread: boolean;
onMoveUp: (groupId: string) => void;
onMoveDown: (groupId: string) => void;
}> = ({ notificationGroupId, unread, onMoveUp, onMoveDown }) => {
const notificationGroup = useAppSelector((state) =>
state.notificationGroups.groups.find(
(item) => item.type !== 'gap' && item.group_key === notificationGroupId,
),
);
const dispatch = useAppDispatch();
const accountId =
notificationGroup?.type === 'gap'
? undefined
: notificationGroup?.sampleAccountIds[0];
const handlers = useMemo(
() => ({
moveUp: () => {
onMoveUp(notificationGroupId);
},
moveDown: () => {
onMoveDown(notificationGroupId);
},
openProfile: () => {
if (accountId) dispatch(navigateToProfile(accountId));
},
mention: () => {
if (accountId) dispatch(mentionComposeById(accountId));
},
}),
[dispatch, notificationGroupId, accountId, onMoveUp, onMoveDown],
);
if (!notificationGroup || notificationGroup.type === 'gap') return null;
let content;
switch (notificationGroup.type) {
case 'reblog':
content = (
<NotificationReblog unread={unread} notification={notificationGroup} />
);
break;
case 'favourite':
content = (
<NotificationFavourite
unread={unread}
notification={notificationGroup}
/>
);
break;
case 'emoji_reaction':
content = (
<NotificationEmojiReaction
unread={unread}
notification={notificationGroup}
/>
);
break;
case 'severed_relationships':
content = (
<NotificationSeveredRelationships
unread={unread}
notification={notificationGroup}
/>
);
break;
case 'mention':
content = (
<NotificationMention unread={unread} notification={notificationGroup} />
);
break;
case 'status_reference':
content = (
<NotificationStatusReference
unread={unread}
notification={notificationGroup}
/>
);
break;
case 'follow':
content = (
<NotificationFollow unread={unread} notification={notificationGroup} />
);
break;
case 'follow_request':
content = (
<NotificationFollowRequest
unread={unread}
notification={notificationGroup}
/>
);
break;
case 'poll':
content = (
<NotificationPoll unread={unread} notification={notificationGroup} />
);
break;
case 'status':
content = (
<NotificationStatus unread={unread} notification={notificationGroup} />
);
break;
case 'update':
content = (
<NotificationUpdate unread={unread} notification={notificationGroup} />
);
break;
case 'admin.sign_up':
content = (
<NotificationAdminSignUp
unread={unread}
notification={notificationGroup}
/>
);
break;
case 'admin.report':
content = (
<NotificationAdminReport
unread={unread}
notification={notificationGroup}
/>
);
break;
case 'moderation_warning':
content = (
<NotificationModerationWarning
unread={unread}
notification={notificationGroup}
/>
);
break;
default:
return null;
}
return <HotKeys handlers={handlers}>{content}</HotKeys>;
};

View file

@ -0,0 +1,113 @@
import { useMemo } from 'react';
import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'mastodon/actions/compose';
import { navigateToStatus } from 'mastodon/actions/statuses';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { useAppDispatch } from 'mastodon/store';
import { AvatarGroup } from './avatar_group';
import { EmbeddedStatus } from './embedded_status';
import { NamesList } from './names_list';
export type LabelRenderer = (
values: Record<string, React.ReactNode>,
) => JSX.Element;
export const NotificationGroupWithStatus: React.FC<{
icon: IconProp;
iconId: string;
statusId?: string;
actions?: JSX.Element;
count: number;
accountIds: string[];
timestamp: string;
labelRenderer: LabelRenderer;
labelSeeMoreHref?: string;
type: string;
unread: boolean;
}> = ({
icon,
iconId,
timestamp,
accountIds,
actions,
count,
statusId,
labelRenderer,
labelSeeMoreHref,
type,
unread,
}) => {
const dispatch = useAppDispatch();
const label = useMemo(
() =>
labelRenderer({
name: (
<NamesList
accountIds={accountIds}
total={count}
seeMoreHref={labelSeeMoreHref}
/>
),
}),
[labelRenderer, accountIds, count, labelSeeMoreHref],
);
const handlers = useMemo(
() => ({
open: () => {
dispatch(navigateToStatus(statusId));
},
reply: () => {
dispatch(replyComposeById(statusId));
},
}),
[dispatch, statusId],
);
return (
<HotKeys handlers={handlers}>
<div
role='button'
className={classNames(
`notification-group focusable notification-group--${type}`,
{ 'notification-group--unread': unread },
)}
tabIndex={0}
>
<div className='notification-group__icon'>
<Icon icon={icon} id={iconId} />
</div>
<div className='notification-group__main'>
<div className='notification-group__main__header'>
<div className='notification-group__main__header__wrapper'>
<AvatarGroup accountIds={accountIds} />
{actions}
</div>
<div className='notification-group__main__header__label'>
{label}
{timestamp && <RelativeTimestamp timestamp={timestamp} />}
</div>
</div>
{statusId && (
<div className='notification-group__main__status'>
<EmbeddedStatus statusId={statusId} />
</div>
)}
</div>
</div>
</HotKeys>
);
};

View file

@ -0,0 +1,55 @@
import { FormattedMessage } from 'react-intl';
import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
import type { StatusVisibility } from 'mastodon/api_types/statuses';
import type { NotificationGroupMention } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationWithStatus } from './notification_with_status';
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.mention'
defaultMessage='{name} mentioned you'
values={values}
/>
);
const privateMentionLabelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.private_mention'
defaultMessage='{name} privately mentioned you'
values={values}
/>
);
export const NotificationMention: React.FC<{
notification: NotificationGroupMention;
unread: boolean;
}> = ({ notification, unread }) => {
const statusVisibility = useAppSelector(
(state) =>
state.statuses.getIn([
notification.statusId,
'visibility',
]) as StatusVisibility,
);
return (
<NotificationWithStatus
type='mention'
icon={ReplyIcon}
iconId='reply'
accountIds={notification.sampleAccountIds}
count={notification.notifications_count}
statusId={notification.statusId}
labelRenderer={
statusVisibility === 'direct'
? privateMentionLabelRenderer
: labelRenderer
}
unread={unread}
/>
);
};

View file

@ -0,0 +1,13 @@
import { ModerationWarning } from 'mastodon/features/notifications/components/moderation_warning';
import type { NotificationGroupModerationWarning } from 'mastodon/models/notification_group';
export const NotificationModerationWarning: React.FC<{
notification: NotificationGroupModerationWarning;
unread: boolean;
}> = ({ notification: { moderationWarning }, unread }) => (
<ModerationWarning
action={moderationWarning.action}
id={moderationWarning.id}
unread={unread}
/>
);

View file

@ -0,0 +1,41 @@
import { FormattedMessage } from 'react-intl';
import BarChart4BarsIcon from '@/material-icons/400-20px/bar_chart_4_bars.svg?react';
import { me } from 'mastodon/initial_state';
import type { NotificationGroupPoll } from 'mastodon/models/notification_group';
import { NotificationWithStatus } from './notification_with_status';
const labelRendererOther = () => (
<FormattedMessage
id='notification.poll'
defaultMessage='A poll you voted in has ended'
/>
);
const labelRendererOwn = () => (
<FormattedMessage
id='notification.own_poll'
defaultMessage='Your poll has ended'
/>
);
export const NotificationPoll: React.FC<{
notification: NotificationGroupPoll;
unread: boolean;
}> = ({ notification, unread }) => (
<NotificationWithStatus
type='poll'
icon={BarChart4BarsIcon}
iconId='bar-chart-4-bars'
accountIds={notification.sampleAccountIds}
count={notification.notifications_count}
statusId={notification.statusId}
labelRenderer={
notification.sampleAccountIds[0] === me
? labelRendererOwn
: labelRendererOther
}
unread={unread}
/>
);

View file

@ -0,0 +1,45 @@
import { FormattedMessage } from 'react-intl';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import type { NotificationGroupReblog } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.reblog'
defaultMessage='{name} boosted your status'
values={values}
/>
);
export const NotificationReblog: React.FC<{
notification: NotificationGroupReblog;
unread: boolean;
}> = ({ notification, unread }) => {
const { statusId } = notification;
const statusAccount = useAppSelector(
(state) =>
state.accounts.get(state.statuses.getIn([statusId, 'account']) as string)
?.acct,
);
return (
<NotificationGroupWithStatus
type='reblog'
icon={RepeatIcon}
iconId='repeat'
accountIds={notification.sampleAccountIds}
statusId={notification.statusId}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}
labelRenderer={labelRenderer}
labelSeeMoreHref={
statusAccount ? `/@${statusAccount}/${statusId}/reblogs` : undefined
}
unread={unread}
/>
);
};

View file

@ -0,0 +1,15 @@
import { RelationshipsSeveranceEvent } from 'mastodon/features/notifications/components/relationships_severance_event';
import type { NotificationGroupSeveredRelationships } from 'mastodon/models/notification_group';
export const NotificationSeveredRelationships: React.FC<{
notification: NotificationGroupSeveredRelationships;
unread: boolean;
}> = ({ notification: { event }, unread }) => (
<RelationshipsSeveranceEvent
type={event.type}
target={event.target_name}
followersCount={event.followers_count}
followingCount={event.following_count}
unread={unread}
/>
);

View file

@ -0,0 +1,31 @@
import { FormattedMessage } from 'react-intl';
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
import type { NotificationGroupStatus } from 'mastodon/models/notification_group';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationWithStatus } from './notification_with_status';
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.status'
defaultMessage='{name} just posted'
values={values}
/>
);
export const NotificationStatus: React.FC<{
notification: NotificationGroupStatus;
unread: boolean;
}> = ({ notification, unread }) => (
<NotificationWithStatus
type='status'
icon={NotificationsActiveIcon}
iconId='notifications-active'
accountIds={notification.sampleAccountIds}
count={notification.notifications_count}
statusId={notification.statusId}
labelRenderer={labelRenderer}
unread={unread}
/>
);

View file

@ -0,0 +1,34 @@
import { FormattedMessage } from 'react-intl';
import ReferenceIcon from '@/material-icons/400-24px/link.svg?react';
import type { NotificationGroupStatusReference } from 'mastodon/models/notification_group';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationWithStatus } from './notification_with_status';
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.status_reference'
defaultMessage='{name} quoted your post'
values={values}
/>
);
export const NotificationStatusReference: React.FC<{
notification: NotificationGroupStatusReference;
unread: boolean;
}> = ({ notification, unread }) => {
return (
<NotificationWithStatus
type='status_reference'
icon={ReferenceIcon}
iconId='reply'
accountIds={notification.sampleAccountIds}
count={notification.notifications_count}
statusId={notification.statusId}
labelRenderer={labelRenderer}
unread={unread}
muted
/>
);
};

View file

@ -0,0 +1,31 @@
import { FormattedMessage } from 'react-intl';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import type { NotificationGroupUpdate } from 'mastodon/models/notification_group';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationWithStatus } from './notification_with_status';
const labelRenderer: LabelRenderer = (values) => (
<FormattedMessage
id='notification.update'
defaultMessage='{name} edited a post'
values={values}
/>
);
export const NotificationUpdate: React.FC<{
notification: NotificationGroupUpdate;
unread: boolean;
}> = ({ notification, unread }) => (
<NotificationWithStatus
type='update'
icon={EditIcon}
iconId='edit'
accountIds={notification.sampleAccountIds}
count={notification.notifications_count}
statusId={notification.statusId}
labelRenderer={labelRenderer}
unread={unread}
/>
);

View file

@ -0,0 +1,115 @@
import { useMemo } from 'react';
import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import {
navigateToStatus,
toggleStatusSpoilers,
} from 'mastodon/actions/statuses';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import Status from 'mastodon/containers/status_container';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { NamesList } from './names_list';
import type { LabelRenderer } from './notification_group_with_status';
export const NotificationWithStatus: React.FC<{
type: string;
icon: IconProp;
iconId: string;
accountIds: string[];
statusId: string;
count: number;
labelRenderer: LabelRenderer;
unread: boolean;
muted?: boolean;
}> = ({
icon,
iconId,
accountIds,
statusId,
count,
labelRenderer,
type,
unread,
muted,
}) => {
const dispatch = useAppDispatch();
const label = useMemo(
() =>
labelRenderer({
name: <NamesList accountIds={accountIds} total={count} />,
}),
[labelRenderer, accountIds, count],
);
const isPrivateMention = useAppSelector(
(state) => state.statuses.getIn([statusId, 'visibility']) === 'direct',
);
const handlers = useMemo(
() => ({
open: () => {
dispatch(navigateToStatus(statusId));
},
reply: () => {
dispatch(replyComposeById(statusId));
},
boost: () => {
dispatch(toggleReblog(statusId));
},
favourite: () => {
dispatch(toggleFavourite(statusId));
},
toggleHidden: () => {
dispatch(toggleStatusSpoilers(statusId));
},
}),
[dispatch, statusId],
);
return (
<HotKeys handlers={handlers}>
<div
role='button'
className={classNames(
`notification-ungrouped focusable notification-ungrouped--${type}`,
{
'notification-ungrouped--unread': unread,
'notification-ungrouped--direct': isPrivateMention,
},
)}
tabIndex={0}
>
<div className='notification-ungrouped__header'>
<div className='notification-ungrouped__header__icon'>
<Icon icon={icon} id={iconId} />
</div>
{label}
</div>
<Status
// @ts-expect-error -- <Status> is not yet typed
id={statusId}
contextType='notifications'
withDismiss
skipPrepend
avatarSize={40}
unfocusable
muted={muted}
withoutEmojiReactions
/>
</div>
</HotKeys>
);
};

View file

@ -0,0 +1,171 @@
import type { PropsWithChildren } from 'react';
import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import ReferenceIcon from '@/material-icons/400-24px/link.svg?react';
import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { setNotificationsFilter } from 'mastodon/actions/notification_groups';
import { Icon } from 'mastodon/components/icon';
import {
selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsQuickFilterAdvanced,
} from 'mastodon/selectors/settings';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const tooltips = defineMessages({
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: {
id: 'notifications.filter.favourites',
defaultMessage: 'Favorites',
},
emojiReactions: {
id: 'notifications.filter.emoji_reactions',
defaultMessage: 'Stamps',
},
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
status_references: {
id: 'notifications.filter.status_references',
defaultMessage: 'Status references',
},
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
statuses: {
id: 'notifications.filter.statuses',
defaultMessage: 'Updates from people you follow',
},
});
const BarButton: React.FC<
PropsWithChildren<{
selectedFilter: string;
type: string;
title?: string;
}>
> = ({ selectedFilter, type, title, children }) => {
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
void dispatch(setNotificationsFilter({ filterType: type }));
}, [dispatch, type]);
return (
<button
className={selectedFilter === type ? 'active' : ''}
onClick={onClick}
title={title}
>
{children}
</button>
);
};
export const FilterBar: React.FC = () => {
const intl = useIntl();
const selectedFilter = useAppSelector(
selectSettingsNotificationsQuickFilterActive,
);
const advancedMode = useAppSelector(
selectSettingsNotificationsQuickFilterAdvanced,
);
if (advancedMode)
return (
<div className='notification__filter-bar'>
<BarButton selectedFilter={selectedFilter} type='all' key='all'>
<FormattedMessage
id='notifications.filter.all'
defaultMessage='All'
/>
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='mention'
key='mention'
title={intl.formatMessage(tooltips.mentions)}
>
<Icon id='reply-all' icon={ReplyAllIcon} />
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='favourite'
key='favourite'
title={intl.formatMessage(tooltips.favourites)}
>
<Icon id='star' icon={StarIcon} />
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='emoji_reaction'
key='emoji_reaction'
title={intl.formatMessage(tooltips.emojiReactions)}
>
<Icon id='smile-o' icon={EmojiReactionIcon} />
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='reblog'
key='reblog'
title={intl.formatMessage(tooltips.boosts)}
>
<Icon id='retweet' icon={RepeatIcon} />
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='status_reference'
key='status_reference'
title={intl.formatMessage(tooltips.status_references)}
>
<Icon id='retweet' icon={ReferenceIcon} />
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='poll'
key='poll'
title={intl.formatMessage(tooltips.polls)}
>
<Icon id='tasks' icon={InsertChartIcon} />
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='status'
key='status'
title={intl.formatMessage(tooltips.statuses)}
>
<Icon id='home' icon={HomeIcon} />
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='follow'
key='follow'
title={intl.formatMessage(tooltips.follows)}
>
<Icon id='user-plus' icon={PersonAddIcon} />
</BarButton>
</div>
);
else
return (
<div className='notification__filter-bar'>
<BarButton selectedFilter={selectedFilter} type='all' key='all'>
<FormattedMessage
id='notifications.filter.all'
defaultMessage='All'
/>
</BarButton>
<BarButton selectedFilter={selectedFilter} type='mention' key='mention'>
<FormattedMessage
id='notifications.filter.mentions'
defaultMessage='Mentions'
/>
</BarButton>
</div>
);
};

View file

@ -0,0 +1,354 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import { useDebouncedCallback } from 'use-debounce';
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
import {
fetchNotificationsGap,
updateScrollPosition,
loadPending,
markNotificationsAsRead,
mountNotifications,
unmountNotifications,
} from 'mastodon/actions/notification_groups';
import { compareId } from 'mastodon/compare_id';
import { Icon } from 'mastodon/components/icon';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import { useIdentity } from 'mastodon/identity_context';
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
import {
selectUnreadNotificationGroupsCount,
selectPendingNotificationGroupsCount,
} from 'mastodon/selectors/notifications';
import {
selectNeedsNotificationPermission,
selectSettingsNotificationsExcludedTypes,
selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsQuickFilterShow,
selectSettingsNotificationsShowUnread,
} from 'mastodon/selectors/settings';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import type { RootState } from 'mastodon/store';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { submitMarkers } from '../../actions/markers';
import Column from '../../components/column';
import { ColumnHeader } from '../../components/column_header';
import { LoadGap } from '../../components/load_gap';
import ScrollableList from '../../components/scrollable_list';
import { FilteredNotificationsBanner } from '../notifications/components/filtered_notifications_banner';
import NotificationsPermissionBanner from '../notifications/components/notifications_permission_banner';
import ColumnSettingsContainer from '../notifications/containers/column_settings_container';
import { NotificationGroup } from './components/notification_group';
import { FilterBar } from './filter_bar';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
markAsRead: {
id: 'notifications.mark_as_read',
defaultMessage: 'Mark every notification as read',
},
});
const getNotifications = createSelector(
[
selectSettingsNotificationsQuickFilterShow,
selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsExcludedTypes,
(state: RootState) => state.notificationGroups.groups,
],
(showFilterBar, allowedType, excludedTypes, notifications) => {
if (!showFilterBar || allowedType === 'all') {
// used if user changed the notification settings after loading the notifications from the server
// otherwise a list of notifications will come pre-filtered from the backend
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
return notifications.filter(
(item) => item.type === 'gap' || !excludedTypes.includes(item.type),
);
}
return notifications.filter(
(item) => item.type === 'gap' || allowedType === item.type,
);
},
);
export const Notifications: React.FC<{
columnId?: string;
multiColumn?: boolean;
}> = ({ columnId, multiColumn }) => {
const intl = useIntl();
const notifications = useAppSelector(getNotifications);
const dispatch = useAppDispatch();
const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
const hasMore = notifications.at(-1)?.type === 'gap';
const lastReadId = useAppSelector((s) =>
selectSettingsNotificationsShowUnread(s)
? s.notificationGroups.lastReadId
: '0',
);
const numPending = useAppSelector(selectPendingNotificationGroupsCount);
const unreadNotificationsCount = useAppSelector(
selectUnreadNotificationGroupsCount,
);
const isUnread = unreadNotificationsCount > 0;
const canMarkAsRead =
useAppSelector(selectSettingsNotificationsShowUnread) &&
unreadNotificationsCount > 0;
const needsNotificationPermission = useAppSelector(
selectNeedsNotificationPermission,
);
const columnRef = useRef<Column>(null);
const selectChild = useCallback((index: number, alignTop: boolean) => {
const container = columnRef.current?.node as HTMLElement | undefined;
if (!container) return;
const element = container.querySelector<HTMLElement>(
`article:nth-of-type(${index + 1}) .focusable`,
);
if (element) {
if (alignTop && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (
!alignTop &&
container.scrollTop + container.clientHeight <
element.offsetTop + element.offsetHeight
) {
element.scrollIntoView(false);
}
element.focus();
}
}, []);
// Keep track of mounted components for unread notification handling
useEffect(() => {
dispatch(mountNotifications());
return () => {
dispatch(unmountNotifications());
dispatch(updateScrollPosition({ top: false }));
};
}, [dispatch]);
const handleLoadGap = useCallback(
(gap: NotificationGap) => {
void dispatch(fetchNotificationsGap({ gap }));
},
[dispatch],
);
const handleLoadOlder = useDebouncedCallback(
() => {
const gap = notifications.at(-1);
if (gap?.type === 'gap') void dispatch(fetchNotificationsGap({ gap }));
},
300,
{ leading: true },
);
const handleLoadPending = useCallback(() => {
dispatch(loadPending());
}, [dispatch]);
const handleScrollToTop = useDebouncedCallback(() => {
dispatch(updateScrollPosition({ top: true }));
}, 100);
const handleScroll = useDebouncedCallback(() => {
dispatch(updateScrollPosition({ top: false }));
}, 100);
useEffect(() => {
return () => {
handleLoadOlder.cancel();
handleScrollToTop.cancel();
handleScroll.cancel();
};
}, [handleLoadOlder, handleScrollToTop, handleScroll]);
const handlePin = useCallback(() => {
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('NOTIFICATIONS', {}));
}
}, [columnId, dispatch]);
const handleMove = useCallback(
(dir: unknown) => {
dispatch(moveColumn(columnId, dir));
},
[dispatch, columnId],
);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);
const handleMoveUp = useCallback(
(id: string) => {
const elementIndex =
notifications.findIndex(
(item) => item.type !== 'gap' && item.group_key === id,
) - 1;
selectChild(elementIndex, true);
},
[notifications, selectChild],
);
const handleMoveDown = useCallback(
(id: string) => {
const elementIndex =
notifications.findIndex(
(item) => item.type !== 'gap' && item.group_key === id,
) + 1;
selectChild(elementIndex, false);
},
[notifications, selectChild],
);
const handleMarkAsRead = useCallback(() => {
dispatch(markNotificationsAsRead());
void dispatch(submitMarkers({ immediate: true }));
}, [dispatch]);
const pinned = !!columnId;
const emptyMessage = (
<FormattedMessage
id='empty_column.notifications'
defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here."
/>
);
const { signedIn } = useIdentity();
const filterBar = signedIn ? <FilterBar /> : null;
const scrollableContent = useMemo(() => {
if (notifications.length === 0 && !hasMore) return null;
return notifications.map((item) =>
item.type === 'gap' ? (
<LoadGap
key={`${item.maxId}-${item.sinceId}`}
disabled={isLoading}
param={item}
onClick={handleLoadGap}
/>
) : (
<NotificationGroup
key={item.group_key}
notificationGroupId={item.group_key}
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
unread={
lastReadId !== '0' &&
!!item.page_max_id &&
compareId(item.page_max_id, lastReadId) > 0
}
/>
),
);
}, [
notifications,
isLoading,
hasMore,
lastReadId,
handleLoadGap,
handleMoveUp,
handleMoveDown,
]);
const prepend = (
<>
{needsNotificationPermission && <NotificationsPermissionBanner />}
<FilteredNotificationsBanner />
</>
);
const scrollContainer = signedIn ? (
<ScrollableList
scrollKey={`notifications-${columnId}`}
trackScroll={!pinned}
isLoading={isLoading}
showLoading={isLoading && notifications.length === 0}
hasMore={hasMore}
numPending={numPending}
prepend={prepend}
alwaysPrepend
emptyMessage={emptyMessage}
onLoadMore={handleLoadOlder}
onLoadPending={handleLoadPending}
onScrollToTop={handleScrollToTop}
onScroll={handleScroll}
bindToDocument={!multiColumn}
>
{scrollableContent}
</ScrollableList>
) : (
<NotSignedInIndicator />
);
const extraButton = canMarkAsRead ? (
<button
aria-label={intl.formatMessage(messages.markAsRead)}
title={intl.formatMessage(messages.markAsRead)}
onClick={handleMarkAsRead}
className='column-header__button'
>
<Icon id='done-all' icon={DoneAllIcon} />
</button>
) : null;
return (
<Column
bindToDocument={!multiColumn}
ref={columnRef}
label={intl.formatMessage(messages.title)}
>
<ColumnHeader
icon='bell'
iconComponent={NotificationsIcon}
active={isUnread}
title={intl.formatMessage(messages.title)}
onPin={handlePin}
onMove={handleMove}
onClick={handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
extraButton={extraButton}
>
<ColumnSettingsContainer />
</ColumnHeader>
{filterBar}
{scrollContainer}
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default Notifications;

View file

@ -0,0 +1,13 @@
import Notifications from 'mastodon/features/notifications';
import Notifications_v2 from 'mastodon/features/notifications_v2';
import { useAppSelector } from 'mastodon/store';
export const NotificationsWrapper = (props) => {
const optedInGroupedNotifications = useAppSelector((state) => state.getIn(['settings', 'notifications', 'groupingBeta'], false));
return (
optedInGroupedNotifications ? <Notifications_v2 {...props} /> : <Notifications {...props} />
);
};
export default NotificationsWrapper;

View file

@ -3,7 +3,7 @@ import { useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Helmet } from 'react-helmet';
import { Link, Switch, Route, useHistory } from 'react-router-dom';
import { Link, Switch, Route } from 'react-router-dom';
import { useDispatch } from 'react-redux';
@ -35,11 +35,10 @@ const Onboarding = () => {
const account = useAppSelector(state => state.getIn(['accounts', me]));
const dispatch = useDispatch();
const intl = useIntl();
const history = useHistory();
const handleComposeClick = useCallback(() => {
dispatch(focusCompose(history, intl.formatMessage(messages.template)));
}, [dispatch, intl, history]);
dispatch(focusCompose(intl.formatMessage(messages.template)));
}, [dispatch, intl]);
return (
<Column>

View file

@ -15,11 +15,11 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { replyCompose } from 'mastodon/actions/compose';
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal';
import { IconButton } from 'mastodon/components/icon_button';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { me, boostModal } from 'mastodon/initial_state';
import { me } from 'mastodon/initial_state';
import { makeGetStatus } from 'mastodon/selectors';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -61,13 +61,13 @@ class Footer extends ImmutablePureComponent {
};
_performReply = () => {
const { dispatch, status, onClose, history } = this.props;
const { dispatch, status, onClose } = this.props;
if (onClose) {
onClose(true);
}
dispatch(replyCompose(status, history));
dispatch(replyCompose(status));
};
handleReplyClick = () => {
@ -104,11 +104,7 @@ class Footer extends ImmutablePureComponent {
const { signedIn } = this.props.identity;
if (signedIn) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
dispatch(toggleFavourite(status.get('id')));
} else {
dispatch(openModal({
modalType: 'INTERACTION',
@ -121,23 +117,12 @@ class Footer extends ImmutablePureComponent {
}
};
_performReblog = (status, privacy) => {
const { dispatch } = this.props;
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
};
handleReblogClick = e => {
const { dispatch, status } = this.props;
const { signedIn } = this.props.identity;
if (signedIn) {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else if ((e && e.shiftKey) || !boostModal) {
this._performReblog(status);
} else {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this._performReblog } }));
}
dispatch(toggleReblog(status.get('id'), e && e.shiftKey));
} else {
dispatch(openModal({
modalType: 'INTERACTION',

View file

@ -4,7 +4,7 @@ import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
@ -109,10 +109,6 @@ class ActionBar extends PureComponent {
...WithRouterPropTypes,
};
handleOpenMentions = () => {
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}/mentioned_users`);
};
handleReplyClick = () => {
this.props.onReply(this.props.status);
};
@ -142,23 +138,23 @@ class ActionBar extends PureComponent {
};
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.props.history);
this.props.onDelete(this.props.status);
};
handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.props.history, true);
this.props.onDelete(this.props.status, true);
};
handleEditClick = () => {
this.props.onEdit(this.props.status, this.props.history);
this.props.onEdit(this.props.status);
};
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.props.history);
this.props.onDirect(this.props.status.get('account'));
};
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.props.history);
this.props.onMention(this.props.status.get('account'));
};
handleMuteClick = () => {
@ -293,7 +289,7 @@ class ActionBar extends PureComponent {
}
if (status.get('limited_scope') !== 'reply') {
menu.push({ text: intl.formatMessage(messages.mentions), action: this.handleOpenMentions });
menu.push({ text: intl.formatMessage(messages.mentions), href: `/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}/mentioned_users` });
}
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
@ -446,4 +442,4 @@ class ActionBar extends PureComponent {
}
export default withRouter(connect(mapStateToProps)(withIdentity(injectIntl(ActionBar))));
export default connect(mapStateToProps)(withIdentity(injectIntl(ActionBar)));

View file

@ -10,10 +10,8 @@ import {
directCompose,
} from '../../../actions/compose';
import {
reblog,
favourite,
unreblog,
unfavourite,
toggleReblog,
toggleFavourite,
pin,
unpin,
emojiReact,
@ -26,10 +24,9 @@ import {
muteStatus,
unmuteStatus,
deleteStatus,
hideStatus,
revealStatus,
toggleStatusSpoilers,
} from '../../../actions/statuses';
import { boostModal, deleteModal } from '../../../initial_state';
import { deleteModal } from '../../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors';
import DetailedStatus from '../components/detailed_status';
@ -57,7 +54,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({
onReply (status, router) {
onReply (status) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
@ -66,45 +63,25 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)),
onConfirm: () => dispatch(replyCompose(status)),
},
}));
} else {
dispatch(replyCompose(status, router));
dispatch(replyCompose(status));
}
});
},
onModalReblog (status, privacy) {
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
},
onReblog (status, e) {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
if (e.shiftKey || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
}
}
dispatch(toggleReblog(status.get('id'), e.shiftKey));
},
onReblogForceModal (status) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
}
dispatch(toggleReblog(status.get('id'), true, true));
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
dispatch(toggleFavourite(status.get('id')));
},
onEmojiReact (status, emoji) {
@ -133,27 +110,27 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}));
},
onDelete (status, history, withRedraft = false) {
onDelete (status, withRedraft = false) {
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
dispatch(deleteStatus(status.get('id'), withRedraft));
} else {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
},
}));
}
},
onDirect (account, router) {
dispatch(directCompose(account, router));
onDirect (account) {
dispatch(directCompose(account));
},
onMention (account, router) {
dispatch(mentionCompose(account, router));
onMention (account) {
dispatch(mentionCompose(account));
},
onOpenMedia (media, index, lang) {
@ -192,11 +169,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')));
} else {
dispatch(hideStatus(status.get('id')));
}
dispatch(toggleStatusSpoilers(status.get('id')));
},
});

View file

@ -39,14 +39,12 @@ import {
unblockDomain,
} from '../../actions/domain_blocks';
import {
favourite,
unfavourite,
emojiReact,
unEmojiReact,
toggleFavourite,
bookmark,
unbookmark,
reblog,
unreblog,
toggleReblog,
pin,
unpin,
} from '../../actions/interactions';
@ -67,7 +65,7 @@ import {
import ColumnHeader from '../../components/column_header';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import StatusContainer from '../../containers/status_container';
import { bookmarkCategoryNeeded, boostModal, deleteModal } from '../../initial_state';
import { bookmarkCategoryNeeded, deleteModal } from '../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import Column from '../ui/components/column';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
@ -257,11 +255,7 @@ class Status extends ImmutablePureComponent {
const { signedIn } = this.props.identity;
if (signedIn) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
dispatch(toggleFavourite(status.get('id')));
} else {
dispatch(openModal({
modalType: 'INTERACTION',
@ -316,11 +310,11 @@ class Status extends ImmutablePureComponent {
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, this.props.history)),
onConfirm: () => dispatch(replyCompose(status)),
},
}));
} else {
dispatch(replyCompose(status, this.props.history));
dispatch(replyCompose(status));
}
} else {
dispatch(openModal({
@ -334,24 +328,12 @@ class Status extends ImmutablePureComponent {
}
};
handleModalReblog = (status, privacy) => {
this.props.dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
};
handleReblogClick = (status, e, force = false) => {
const { dispatch } = this.props;
const { signedIn } = this.props.identity;
if (signedIn) {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
if (!force && ((e && e.shiftKey) || !boostModal)) {
this.handleModalReblog(status);
} else {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.handleModalReblog } }));
}
}
dispatch(toggleReblog(status.get('id'), e && e.shiftKey, force));
} else {
dispatch(openModal({
modalType: 'INTERACTION',
@ -398,33 +380,33 @@ class Status extends ImmutablePureComponent {
}));
};
handleDeleteClick = (status, history, withRedraft = false) => {
handleDeleteClick = (status, withRedraft = false) => {
const { dispatch, intl } = this.props;
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
dispatch(deleteStatus(status.get('id'), withRedraft));
} else {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
},
}));
}
};
handleEditClick = (status, history) => {
this.props.dispatch(editStatus(status.get('id'), history));
handleEditClick = (status) => {
this.props.dispatch(editStatus(status.get('id')));
};
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
handleDirectClick = (account) => {
this.props.dispatch(directCompose(account));
};
handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router));
handleMentionClick = (account) => {
this.props.dispatch(mentionCompose(account));
};
handleOpenMedia = (media, index, lang) => {

View file

@ -10,7 +10,7 @@ import { scrollRight } from '../../../scroll';
import BundleContainer from '../containers/bundle_container';
import {
Compose,
Notifications,
NotificationsWrapper,
HomeTimeline,
CommunityTimeline,
PublicTimeline,
@ -37,7 +37,7 @@ import NavigationPanel from './navigation_panel';
const componentMap = {
'COMPOSE': Compose,
'HOME': HomeTimeline,
'NOTIFICATIONS': Notifications,
'NOTIFICATIONS': NotificationsWrapper,
'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline,

View file

@ -36,6 +36,7 @@ import { NavigationPortal } from 'mastodon/components/navigation_portal';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { enableDtlMenu, timelinePreview, trendsEnabled, dtlTag, enableLocalTimeline, isHideItem } from 'mastodon/initial_state';
import { transientSingleColumn } from 'mastodon/is_mobile';
import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications';
import ColumnLink from './column_link';
import DisabledAccountBanner from './disabled_account_banner';
@ -65,15 +66,19 @@ const messages = defineMessages({
});
const NotificationsLink = () => {
const optedInGroupedNotifications = useSelector((state) => state.getIn(['settings', 'notifications', 'groupingBeta'], false));
const count = useSelector(state => state.getIn(['notifications', 'unread']));
const intl = useIntl();
const newCount = useSelector(selectUnreadNotificationGroupsCount);
return (
<ColumnLink
key='notifications'
transparent
to='/notifications'
icon={<IconWithBadge id='bell' icon={NotificationsIcon} count={count} className='column-link__icon' />}
activeIcon={<IconWithBadge id='bell' icon={NotificationsActiveIcon} count={count} className='column-link__icon' />}
icon={<IconWithBadge id='bell' icon={NotificationsIcon} count={optedInGroupedNotifications ? newCount : count} className='column-link__icon' />}
activeIcon={<IconWithBadge id='bell' icon={NotificationsActiveIcon} count={optedInGroupedNotifications ? newCount : count} className='column-link__icon' />}
text={intl.formatMessage(messages.notifications)}
/>
);

View file

@ -13,6 +13,7 @@ import { HotKeys } from 'react-hotkeys';
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
import { initializeNotifications } from 'mastodon/actions/notifications_migration';
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import { HoverCardController } from 'mastodon/components/hover_card_controller';
import { PictureInPicture } from 'mastodon/features/picture_in_picture';
@ -22,7 +23,6 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache';
import { expandNotifications } from '../../actions/notifications';
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
import { expandHomeTimeline } from '../../actions/timelines';
import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards } from '../../initial_state';
@ -53,7 +53,7 @@ import {
DirectTimeline,
HashtagTimeline,
AntennaTimeline,
Notifications,
NotificationsWrapper,
NotificationRequests,
NotificationRequest,
FollowRequests,
@ -84,6 +84,7 @@ import {
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
import '../../components/status';
@ -222,7 +223,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/antennasw/:id' component={AntennaSetting} content={children} />
<WrappedRoute path='/antennast/:id' component={AntennaTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
<WrappedRoute path='/notifications' component={NotificationsWrapper} content={children} exact />
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
<WrappedRoute path='/notifications/requests/:id' component={NotificationRequest} content={children} exact />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
@ -435,7 +436,7 @@ class UI extends PureComponent {
if (signedIn) {
this.props.dispatch(fetchMarkers());
this.props.dispatch(expandHomeTimeline());
this.props.dispatch(expandNotifications());
this.props.dispatch(initializeNotifications());
this.props.dispatch(fetchServerTranslationLanguages());
setTimeout(() => this.props.dispatch(fetchServer()), 3000);

View file

@ -7,7 +7,15 @@ export function Compose () {
}
export function Notifications () {
return import(/* webpackChunkName: "features/notifications" */'../../notifications');
return import(/* webpackChunkName: "features/notifications_v1" */'../../notifications');
}
export function Notifications_v2 () {
return import(/* webpackChunkName: "features/notifications_v2" */'../../notifications_v2');
}
export function NotificationsWrapper () {
return import(/* webpackChunkName: "features/notifications" */'../../notifications_wrapper');
}
export function HomeTimeline () {

View file

@ -342,7 +342,6 @@
"notification.follow_request": "{name} ha solicitau seguir-te",
"notification.mention": "{name} t'ha mencionau",
"notification.own_poll": "La tuya enqüesta ha rematau",
"notification.poll": "Una enqüesta en a quala has votau ha rematau",
"notification.reblog": "{name} ha retutau la tuya publicación",
"notification.status": "{name} acaba de publicar",
"notification.update": "{name} editó una publicación",

View file

@ -482,7 +482,6 @@
"notification.moderation_warning.action_silence": "لقد تم تقييد حسابك.",
"notification.moderation_warning.action_suspend": "لقد تم تعليق حسابك.",
"notification.own_poll": "انتهى استطلاعك للرأي",
"notification.poll": "لقد انتهى استطلاع رأي شاركتَ فيه",
"notification.reblog": "قام {name} بمشاركة منشورك",
"notification.relationships_severance_event": "فقدت الاتصالات مع {name}",
"notification.relationships_severance_event.account_suspension": "قام مشرف من {from} بتعليق {target}، مما يعني أنك لم يعد بإمكانك تلقي التحديثات منهم أو التفاعل معهم.",

View file

@ -1,7 +1,7 @@
{
"about.blocks": "Sirvidores moderaos",
"about.contact": "Contautu:",
"about.disclaimer": "Mastodon ye software gratuito ya de códigu llibre, ya una marca rexistrada de Mastodon gGmbH.",
"about.disclaimer": "Mastodon ye software gratuito y de códigu llibre, y una marca rexistrada de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "El motivu nun ta disponible",
"about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.",
"about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles ya'l conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
@ -37,15 +37,15 @@
"account.hide_reblogs": "Anubrir los artículos compartíos de @{name}",
"account.in_memoriam": "N'alcordanza.",
"account.joined_short": "Data de xunión",
"account.link_verified_on": "La propiedá d'esti enllaz foi comprobada'l {date}",
"account.link_verified_on": "La propiedá d'esti enllaz comprobóse'l {date}",
"account.media": "Multimedia",
"account.mention": "Mentar a @{name}",
"account.moved_to": "{name} indicó qu'agora la so cuenta nueva ye:",
"account.mute": "Desactivar los avisos de @{name}",
"account.open_original_page": "Abrir la páxina orixinal",
"account.posts": "Artículos",
"account.posts_with_replies": "Artículos ya rempuestes",
"account.report": "Informar de: @{name}",
"account.posts_with_replies": "Artículos y rempuestes",
"account.report": "Informar de @{name}",
"account.requested_follow": "{name} solicitó siguite",
"account.show_reblogs": "Amosar los artículos compartíos de @{name}",
"account.unblock": "Desbloquiar a @{name}",
@ -71,7 +71,7 @@
"bundle_column_error.routing.body": "Nun se pudo atopar la páxina solicitada. ¿De xuru que la URL de la barra de direiciones ta bien escrita?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.message": "Asocedió daqué malo mentanto se cargaba esti componente.",
"closed_registrations.other_server_instructions": "Darréu que Mastodon ye una rede social descentralizada, pues crear una cuenta n'otru sirvidor ya siguir interactuando con esti.",
"closed_registrations.other_server_instructions": "Darréu que Mastodon ye una rede social descentralizada, pues crear una cuenta n'otru sirvidor y siguir interactuando con esti.",
"closed_registrations_modal.description": "Anguaño nun ye posible crear cuentes en {domain}, mas ten en cuenta que nun precises una cuenta nesti sirvidor pa usar Mastodon.",
"closed_registrations_modal.find_another_server": "Atopar otru sirvidor",
"closed_registrations_modal.preamble": "Mastodon ye una rede social descentralizada polo que nun importa ónde crees la cuenta, vas ser a siguir ya interactuar con persones d'esti sirvidor. ¡Ya tamién pues tener el to propiu sirvidor!",
@ -107,7 +107,7 @@
"compose_form.lock_disclaimer.lock": "privada",
"compose_form.placeholder": "¿En qué pienses?",
"compose_form.poll.option_placeholder": "Opción {number}",
"compose_form.poll.type": "Estilu",
"compose_form.poll.type": "Tipu",
"compose_form.publish_form": "Artículu nuevu",
"confirmation_modal.cancel": "Encaboxar",
"confirmations.block.confirm": "Bloquiar",
@ -120,7 +120,7 @@
"confirmations.edit.message": "La edición va sobrescribir el mensaxe que tas escribiendo. ¿De xuru que quies siguir?",
"confirmations.logout.confirm": "Zarrar la sesión",
"confirmations.logout.message": "¿De xuru que quies zarrar la sesión?",
"confirmations.redraft.confirm": "Desaniciar ya reeditar",
"confirmations.redraft.confirm": "Desaniciar y reeditar",
"confirmations.reply.confirm": "Responder",
"confirmations.unfollow.confirm": "Dexar de siguir",
"confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?",
@ -140,7 +140,7 @@
"embed.preview": "Va apaecer asina:",
"emoji_button.activity": "Actividá",
"emoji_button.flags": "Banderes",
"emoji_button.food": "Comida ya bébora",
"emoji_button.food": "Comida y bébora",
"emoji_button.nature": "Natura",
"emoji_button.not_found": "Nun s'atoparon fustaxes que concasen",
"emoji_button.objects": "Oxetos",
@ -149,7 +149,7 @@
"emoji_button.search": "Buscar…",
"emoji_button.search_results": "Resultaos de la busca",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viaxes ya llugares",
"emoji_button.travel": "Viaxes y llugares",
"empty_column.account_timeline": "¡Equí nun hai nengún artículu!",
"empty_column.blocks": "Nun bloquiesti a nengún perfil.",
"empty_column.bookmarked_statuses": "Nun tienes nengún artículu en Marcadores. Cuando amiestes dalgún, apaez equí.",
@ -168,7 +168,7 @@
"error.unexpected_crash.explanation": "Pola mor d'un fallu nel códigu o un problema de compatibilidá del restolador, esta páxina nun se pudo amosar correutamente.",
"error.unexpected_crash.explanation_addons": "Esta páxina nun se pudo amosar correutamente. Ye probable que dalgún complementu del restolador o dalguna ferramienta de traducción automática produxere esti error.",
"error.unexpected_crash.next_steps": "Prueba a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.",
"error.unexpected_crash.next_steps_addons": "Prueba a desactivalos ya a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.",
"error.unexpected_crash.next_steps_addons": "Prueba a desactivalos y a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.",
"explore.search_results": "Resultaos de la busca",
"explore.suggested_follows": "Perfiles",
"explore.title": "Esploración",
@ -179,7 +179,7 @@
"filter_modal.added.context_mismatch_title": "¡El contestu nun coincide!",
"filter_modal.added.expired_explanation": "Esta categoría de peñera caducó, tienes de camudar la so data de caducidá p'aplicala.",
"filter_modal.added.expired_title": "¡La peñera caducó!",
"filter_modal.added.review_and_configure": "Pa revisar ya configurar a fondu esta categoría de peñera, vete a la {settings_link}.",
"filter_modal.added.review_and_configure": "Pa revisar y configurar a fondu esta categoría de peñera, vete a la {settings_link}.",
"filter_modal.added.review_and_configure_title": "Configuración de la peñera",
"filter_modal.added.settings_link": "páxina de configuración",
"filter_modal.added.short_explanation": "Esti artículu amestóse a la categoría de peñera siguiente: {title}.",
@ -195,8 +195,10 @@
"follow_request.reject": "Refugar",
"follow_requests.unlocked_explanation": "Magar que la to cuenta nun seya privada, el personal del dominiu «{domain}» pensó qu'a lo meyor quies revisar manualmente les solicitúes de siguimientu d'estes cuentes.",
"follow_suggestions.dismiss": "Nun volver amosar",
"follow_suggestions.friends_of_friends_longer": "Ye popular ente los perfiles que sigues",
"follow_suggestions.personalized_suggestion": "Suxerencia personalizada",
"follow_suggestions.popular_suggestion": "Suxerencia popular",
"follow_suggestions.similar_to_recently_followed_longer": "Aseméyase a los perfiles que siguiesti apocayá",
"follow_suggestions.view_all": "Ver too",
"follow_suggestions.who_to_follow": "A quién siguir",
"footer.about": "Tocante a",
@ -272,6 +274,8 @@
"lists.subheading": "Les tos llistes",
"load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
"media_gallery.toggle_visible": "{number, plural, one {Anubrir la imaxe} other {Anubrir les imáxenes}}",
"name_and_others": "{name} y {count, plural, one {# más} other {# más}}",
"name_and_others_with_link": "{name} y <a>{count, plural, one {# más} other {# más}}</a>",
"navigation_bar.about": "Tocante a",
"navigation_bar.blocks": "Perfiles bloquiaos",
"navigation_bar.bookmarks": "Marcadores",
@ -281,11 +285,11 @@
"navigation_bar.explore": "Esploración",
"navigation_bar.filters": "Pallabres desactivaes",
"navigation_bar.follow_requests": "Solicitúes de siguimientu",
"navigation_bar.follows_and_followers": "Perfiles que sigues ya te siguen",
"navigation_bar.follows_and_followers": "Perfiles que sigues y te siguen",
"navigation_bar.lists": "Llistes",
"navigation_bar.logout": "Zarrar la sesión",
"navigation_bar.mutes": "Perfiles colos avisos desactivaos",
"navigation_bar.opened_in_classic_interface": "Los artículos, les cuentes ya otres páxines específiques ábrense por defeutu na interfaz web clásica.",
"navigation_bar.opened_in_classic_interface": "Los artículos, les cuentes y otres páxines específiques ábrense por defeutu na interfaz web clásica.",
"navigation_bar.pins": "Artículos fixaos",
"navigation_bar.preferences": "Preferencies",
"navigation_bar.public_timeline": "Llinia de tiempu federada",
@ -296,13 +300,13 @@
"notification.follow": "{name} siguióte",
"notification.follow_request": "{name} solicitó siguite",
"notification.mention": "{name} mentóte",
"notification.poll": "Finó una encuesta na que votesti",
"notification.reblog": "{name} compartió'l to artículu",
"notification.status": "{name} ta acabante d'espublizar",
"notification.update": "{name} editó un artículu",
"notifications.clear": "Borrar los avisos",
"notifications.column_settings.admin.report": "Informes nuevos:",
"notifications.column_settings.admin.sign_up": "Rexistros nuevos:",
"notifications.column_settings.beta.category": "Funciones esperimentales",
"notifications.column_settings.follow": "Siguidores nuevos:",
"notifications.column_settings.follow_request": "Solicitúes de siguimientu nueves:",
"notifications.column_settings.mention": "Menciones:",
@ -319,7 +323,7 @@
"notifications.mark_as_read": "Marcar tolos avisos como lleíos",
"notifications.permission_required": "Los avisos d'escritoriu nun tán disponibles porque nun se concedió'l permisu riquíu.",
"onboarding.profile.note_hint": "Pues @mentar a otros perfiles o poner #etiquetes…",
"onboarding.start.lead": "Xá yes parte de Mastodon, una plataforma social multimedia descentralizada onde tu ya non un algoritmu, personalices la to esperiencia. Vamos presentate esti llugar social nuevu:",
"onboarding.start.lead": "Yá yes parte de Mastodon, una plataforma social multimedia descentralizada onde tu y non un algoritmu, personalices la to esperiencia. Vamos presentate esti llugar social nuevu:",
"onboarding.start.skip": "¿Nun precises ayuda pa comenzar?",
"onboarding.steps.follow_people.body": "Mastodon trata namás de siguir a cuentes interesantes.",
"onboarding.steps.publish_status.body": "Saluda al mundu con semeyes, vídeos, testu o encuestes {emoji}",
@ -334,6 +338,8 @@
"poll_button.add_poll": "Amestar una encuesta",
"poll_button.remove_poll": "Quitar la encuesta",
"privacy.change": "Configurar la privacidá del artículu",
"privacy.direct.short": "Perfiles específicos",
"privacy.private.short": "Siguidores",
"privacy.public.short": "Artículu públicu",
"privacy_policy.last_updated": "Data del últimu anovamientu: {date}",
"privacy_policy.title": "Política de privacidá",
@ -383,10 +389,11 @@
"report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:",
"report.thanks.take_action_actionable": "Mentanto revisamos esti informe, pues tomar midíes contra @{name}:",
"report.thanks.title": "¿Nun quies ver esti conteníu?",
"report.thanks.title_actionable": "Gracies pol informe, el casu xá ta n'investigación.",
"report.thanks.title_actionable": "Gracies pol informe, el casu yá ta n'investigación.",
"report.unfollow": "Dexar de siguir a @{name}",
"report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.",
"report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} artículu} other {Axuntáronse {count} artículos}}",
"report_notification.categories.legal_sentence": "conteníu illegal",
"report_notification.open": "Abrir l'informe",
"search.no_recent_searches": "Nun hai nenguna busca recién",
"search.placeholder": "Buscar",
@ -396,6 +403,7 @@
"search.quick_action.status_search": "Artículos que concasen con {x}",
"search.search_or_paste": "Busca o apiega una URL",
"search_popout.language_code": "códigu de llingua ISO",
"search_popout.options": "Opciones de busca",
"search_popout.quick_actions": "Aiciones rápides",
"search_popout.recent": "Busques de recién",
"search_popout.specific_date": "data específica",
@ -440,12 +448,13 @@
"status.reblog": "Compartir",
"status.reblogged_by": "{name} compartió",
"status.reblogs.empty": "Naide nun compartió esti artículu. Cuando daquién lo faiga, apaez equí.",
"status.redraft": "Desaniciar ya reeditar",
"status.redraft": "Desaniciar y reeditar",
"status.replied_to": "En rempuesta a {name}",
"status.reply": "Responder",
"status.replyAll": "Responder al filu",
"status.report": "Informar de @{name}",
"status.sensitive_warning": "Conteníu sensible",
"status.share": "Compartir",
"status.show_filter_reason": "Amosar de toes toes",
"status.show_less": "Amosar menos",
"status.show_more": "Amosar más",
@ -472,7 +481,7 @@
"units.short.thousand": "{count} mil",
"upload_button.label": "Amestar ficheros multimedia",
"upload_error.poll": "La xuba de ficheros nun ta permitida coles encuestes.",
"upload_form.audio_description": "Describi'l conteníu pa persones sordes ya/o ciegues",
"upload_form.audio_description": "Describi'l conteníu pa persones sordes y/o ciegues",
"upload_form.edit": "Editar",
"upload_modal.analyzing_picture": "Analizando la semeya…",
"upload_modal.apply": "Aplicar",

View file

@ -485,7 +485,6 @@
"notification.moderation_warning.action_silence": "Ваш уліковы запіс быў абмежаваны.",
"notification.moderation_warning.action_suspend": "Ваш уліковы запіс быў прыпынены.",
"notification.own_poll": "Ваша апытанне скончылася",
"notification.poll": "Апытанне, дзе вы прынялі ўдзел, скончылася",
"notification.reblog": "{name} пашырыў ваш допіс",
"notification.relationships_severance_event": "Страціў сувязь з {name}",
"notification.relationships_severance_event.account_suspension": "Адміністратар з {from} прыпыніў працу {target}, што азначае, што вы больш не можаце атрымліваць ад іх абнаўлення ці ўзаемадзейнічаць з імі.",

View file

@ -443,6 +443,8 @@
"mute_modal.title": "Заглушавате ли потребител?",
"mute_modal.you_wont_see_mentions": "Няма да виждате споменаващите ги публикации.",
"mute_modal.you_wont_see_posts": "Още могат да виждат публикациите ви, но вие техните не.",
"name_and_others": "{name} и {count, plural, one {# друг} other {# други}}",
"name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a>",
"navigation_bar.about": "Относно",
"navigation_bar.advanced_interface": "Отваряне в разширен уебинтерфейс",
"navigation_bar.blocks": "Блокирани потребители",
@ -470,6 +472,10 @@
"navigation_bar.security": "Сигурност",
"not_signed_in_indicator.not_signed_in": "Трябва ви вход за достъп до ресурса.",
"notification.admin.report": "{name} докладва {target}",
"notification.admin.report_account": "{name} докладва {count, plural, one {публикация} other {# публикации}} от {target} за {category}",
"notification.admin.report_account_other": "{name} докладва {count, plural, one {публикация} other {# публикации}} от {target}",
"notification.admin.report_statuses": "{name} докладва {target} за {category}",
"notification.admin.report_statuses_other": "{name} докладва {target}",
"notification.admin.sign_up": "{name} се регистрира",
"notification.favourite": "{name} направи любима публикацията ви",
"notification.follow": "{name} ви последва",
@ -486,6 +492,7 @@
"notification.moderation_warning.action_suspend": "Вашият акаунт е спрян.",
"notification.own_poll": "Анкетата ви приключи",
"notification.poll": "Анкета, в която гласувахте, приключи",
"notification.private_mention": "{name} лично ви спомена",
"notification.reblog": "{name} подсили ваша публикация",
"notification.relationships_severance_event": "Изгуби се връзката с {name}",
"notification.relationships_severance_event.account_suspension": "Администратор от {from} спря {target}, което значи че повече не може да получавате новости от тях или да взаимодействате с тях.",
@ -503,6 +510,8 @@
"notifications.column_settings.admin.report": "Нови доклади:",
"notifications.column_settings.admin.sign_up": "Нови регистрации:",
"notifications.column_settings.alert": "Известия на работния плот",
"notifications.column_settings.beta.category": "Експериментални функции",
"notifications.column_settings.beta.grouping": "Групови известия",
"notifications.column_settings.favourite": "Любими:",
"notifications.column_settings.filter_bar.advanced": "Показване на всички категории",
"notifications.column_settings.filter_bar.category": "Лента за бърз филтър",
@ -666,9 +675,13 @@
"report.unfollow_explanation": "Последвали сте този акаунт. За да не виждате повече публикациите му в началния си инфопоток, спрете да го следвате.",
"report_notification.attached_statuses": "{count, plural, one {прикаченa {count} публикация} other {прикачени {count} публикации}}",
"report_notification.categories.legal": "Правни въпроси",
"report_notification.categories.legal_sentence": "незаконно съдържание",
"report_notification.categories.other": "Друго",
"report_notification.categories.other_sentence": "друго",
"report_notification.categories.spam": "Спам",
"report_notification.categories.spam_sentence": "спам",
"report_notification.categories.violation": "Нарушение на правилото",
"report_notification.categories.violation_sentence": "нарушение на правило",
"report_notification.open": "Отваряне на доклада",
"search.no_recent_searches": "Няма скорошни търсения",
"search.placeholder": "Търсене",

View file

@ -320,7 +320,6 @@
"notification.follow_request": "{name} আপনাকে অনুসরণ করার জন্য অনুরধ করেছে",
"notification.mention": "{name} আপনাকে উল্লেখ করেছেন",
"notification.own_poll": "আপনার পোল শেষ হয়েছে",
"notification.poll": "আপনি ভোট দিয়েছিলেন এমন এক নির্বাচনের ভোটের সময় শেষ হয়েছে",
"notification.reblog": "{name} আপনার কার্যক্রমে সমর্থন দেখিয়েছেন",
"notifications.clear": "প্রজ্ঞাপনগুলো মুছে ফেলতে",
"notifications.clear_confirmation": "আপনি কি নির্চিত প্রজ্ঞাপনগুলো মুছে ফেলতে চান ?",

View file

@ -398,7 +398,6 @@
"notification.mention": "Gant {name} oc'h bet meneget",
"notification.moderation-warning.learn_more": "Gouzout hiroc'h",
"notification.own_poll": "Echu eo ho sontadeg",
"notification.poll": "Ur sontadeg ho deus mouezhet warnañ a zo echuet",
"notification.reblog": "Gant {name} eo bet skignet ho toud",
"notification.status": "Emañ {name} o paouez toudañ",
"notification.update": "Gant {name} ez eus bet kemmet un toud",

View file

@ -443,6 +443,8 @@
"mute_modal.title": "Silenciem l'usuari?",
"mute_modal.you_wont_see_mentions": "No veureu publicacions que els esmentin.",
"mute_modal.you_wont_see_posts": "Encara poden veure les vostres publicacions, però no veureu les seves.",
"name_and_others": "{name} i {count, plural, one {# altre} other {# altres}}",
"name_and_others_with_link": "{name} i <a>{count, plural, one {# altre} other {# altres}}</a>",
"navigation_bar.about": "Quant a",
"navigation_bar.advanced_interface": "Obre en la interfície web avançada",
"navigation_bar.blocks": "Usuaris blocats",
@ -470,6 +472,10 @@
"navigation_bar.security": "Seguretat",
"not_signed_in_indicator.not_signed_in": "Cal que iniciïs la sessió per a accedir a aquest recurs.",
"notification.admin.report": "{name} ha reportat {target}",
"notification.admin.report_account": "{name} ha reportat {count, plural, one {una publicació} other {# publicacions}} de {target} per {category}",
"notification.admin.report_account_other": "{name} ha reportat {count, plural, one {una publicació} other {# publicacions}} de {target}",
"notification.admin.report_statuses": "{name} ha reportat {target} per {category}",
"notification.admin.report_statuses_other": "{name} ha reportat {target}",
"notification.admin.sign_up": "{name} s'ha registrat",
"notification.favourite": "{name} ha afavorit el teu tut",
"notification.follow": "{name} et segueix",
@ -485,7 +491,8 @@
"notification.moderation_warning.action_silence": "S'ha limitat el vostre compte.",
"notification.moderation_warning.action_suspend": "S'ha suspès el vostre compte.",
"notification.own_poll": "La teva enquesta ha finalitzat",
"notification.poll": "Ha finalitzat una enquesta en què has votat",
"notification.poll": "Ha finalitzat una enquesta que heu respost",
"notification.private_mention": "{name} us ha esmentat en privat",
"notification.reblog": "{name} t'ha impulsat",
"notification.relationships_severance_event": "S'han perdut les connexions amb {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspès {target}; això vol dir que ja no en podreu rebre actualitzacions o interactuar-hi.",
@ -503,6 +510,8 @@
"notifications.column_settings.admin.report": "Nous informes:",
"notifications.column_settings.admin.sign_up": "Registres nous:",
"notifications.column_settings.alert": "Notificacions d'escriptori",
"notifications.column_settings.beta.category": "Característiques experimentals",
"notifications.column_settings.beta.grouping": "Notificacions de grup",
"notifications.column_settings.favourite": "Favorits:",
"notifications.column_settings.filter_bar.advanced": "Mostra totes les categories",
"notifications.column_settings.filter_bar.category": "Barra ràpida de filtres",
@ -666,9 +675,13 @@
"report.unfollow_explanation": "Estàs seguint aquest compte. Per no veure els seus tuts a la teva línia de temps d'Inici, deixa de seguir-lo.",
"report_notification.attached_statuses": "{count, plural, one {{count} tut} other {{count} tuts}} adjunts",
"report_notification.categories.legal": "Legal",
"report_notification.categories.legal_sentence": "contingut no permès",
"report_notification.categories.other": "Altres",
"report_notification.categories.other_sentence": "altres",
"report_notification.categories.spam": "Brossa",
"report_notification.categories.spam_sentence": "brossa",
"report_notification.categories.violation": "Violació de norma",
"report_notification.categories.violation_sentence": "violació de normes",
"report_notification.open": "Obre l'informe",
"search.no_recent_searches": "No hi ha cerques recents",
"search.placeholder": "Cerca",

View file

@ -391,7 +391,6 @@
"notification.follow_request": "{name} داوای کردووە کە شوێنت بکەوێت",
"notification.mention": "{name} باسی ئێوەی کرد",
"notification.own_poll": "ڕاپرسیەکەت کۆتایی هات",
"notification.poll": "ڕاپرسییەک کە دەنگی پێداویت کۆتایی هات",
"notification.reblog": "{name} نووسراوەکەتی دووبارە توتاند",
"notification.status": "{name} تازە بڵاوکرایەوە",
"notification.update": "{name} پۆستێکی دەستکاریکرد",

View file

@ -240,7 +240,6 @@
"notification.follow_request": "{name} vole abbunassi à u vostru contu",
"notification.mention": "{name} v'hà mintuvatu",
"notification.own_poll": "U vostru scandagliu hè compiu",
"notification.poll": "Un scandagliu induve avete vutatu hè finitu",
"notification.reblog": "{name} hà spartutu u vostru statutu",
"notification.status": "{name} hà appena pubblicatu",
"notifications.clear": "Purgà e nutificazione",

View file

@ -485,7 +485,6 @@
"notification.moderation_warning.action_silence": "Váš účet byl omezen.",
"notification.moderation_warning.action_suspend": "Váš účet byl pozastaven.",
"notification.own_poll": "Vaše anketa skončila",
"notification.poll": "Anketa, ve které jste hlasovali, skončila",
"notification.reblog": "Uživatel {name} boostnul váš příspěvek",
"notification.relationships_severance_event": "Kontakt ztracen s {name}",
"notification.relationships_severance_event.account_suspension": "Administrátor z {from} pozastavil {target}, což znamená, že již od nich nemůžete přijímat aktualizace nebo s nimi interagovat.",

View file

@ -48,8 +48,8 @@
"account.mention": "Crybwyll @{name}",
"account.moved_to": "Mae {name} wedi nodi fod eu cyfrif newydd yn:",
"account.mute": "Tewi @{name}",
"account.mute_notifications_short": "Distewi hysbysiadau",
"account.mute_short": "Tewi",
"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.",
@ -92,7 +92,7 @@
"block_modal.they_cant_mention": "Nid ydynt yn gallu eich crybwyll na'ch dilyn.",
"block_modal.they_cant_see_posts": "Nid ydynt yn gallu gweld eich postiadau ac ni fyddwch yn gweld eu rhai hwy.",
"block_modal.they_will_know": "Gallant weld eu bod wedi'u rhwystro.",
"block_modal.title": "Rhwystro defnyddiwr?",
"block_modal.title": "Blocio defnyddiwr?",
"block_modal.you_wont_see_mentions": "Ni welwch bostiadau sy'n sôn amdanynt.",
"boost_modal.combo": "Mae modd pwyso {combo} er mwyn hepgor hyn tro nesa",
"bundle_column_error.copy_stacktrace": "Copïo'r adroddiad gwall",
@ -164,7 +164,7 @@
"compose_form.spoiler.marked": "Dileu rhybudd cynnwys",
"compose_form.spoiler.unmarked": "Ychwanegu rhybudd cynnwys",
"compose_form.spoiler_placeholder": "Rhybudd cynnwys (dewisol)",
"confirmation_modal.cancel": "Diddymu",
"confirmation_modal.cancel": "Canslo",
"confirmations.block.confirm": "Blocio",
"confirmations.cancel_follow_request.confirm": "Tynnu'r cais yn ôl",
"confirmations.cancel_follow_request.message": "Ydych chi'n siŵr eich bod am dynnu'ch cais i ddilyn {name} yn ôl?",
@ -174,7 +174,7 @@
"confirmations.delete_list.message": "Ydych chi'n siŵr eich bod eisiau dileu'r rhestr hwn am byth?",
"confirmations.discard_edit_media.confirm": "Dileu",
"confirmations.discard_edit_media.message": "Mae gennych newidiadau heb eu cadw i'r disgrifiad cyfryngau neu'r rhagolwg - eu dileu beth bynnag?",
"confirmations.domain_block.confirm": "Rhwystro gweinydd",
"confirmations.domain_block.confirm": "Blocio gweinydd",
"confirmations.domain_block.message": "Ydych chi wir, wir eisiau blocio'r holl {domain}? Fel arfer, mae blocio neu dewi pobl penodol yn broses mwy effeithiol. Fyddwch chi ddim yn gweld cynnwys o'r parth hwnnw mewn ffrydiau cyhoeddus neu yn eich hysbysiadau. Bydd eich dilynwyr o'r parth hwnnw yn cael eu ddileu.",
"confirmations.edit.confirm": "Golygu",
"confirmations.edit.message": "Bydd golygu nawr yn trosysgrifennu'r neges rydych yn ei ysgrifennu ar hyn o bryd. Ydych chi'n siŵr eich bod eisiau gwneud hyn?",
@ -201,17 +201,17 @@
"disabled_account_banner.account_settings": "Gosodiadau'r cyfrif",
"disabled_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd.",
"dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl sydd â chyfrifon ar {domain}.",
"dismissable_banner.dismiss": "Diddymu",
"dismissable_banner.dismiss": "Cau",
"dismissable_banner.explore_links": "Dyma straeon newyddion syn cael eu rhannu fwyaf ar y we gymdeithasol heddiw. Mae'r straeon newyddion diweddaraf sy'n cael eu postio gan fwy o unigolion gwahanol yn cael eu graddio'n uwch.",
"dismissable_banner.explore_statuses": "Mae'r rhain yn bostiadau o bob rhan o'r we gymdeithasol sydd ar gynnydd heddiw. Mae postiadau mwy diweddar sydd â mwy o hybiau a ffefrynu'n cael eu graddio'n uwch.",
"dismissable_banner.explore_tags": "Mae'r rhain yn hashnodau sydd ar gynnydd ar y we gymdeithasol heddiw. Mae hashnodau sy'n cael eu defnyddio gan fwy o unigolion gwahanol yn cael eu graddio'n uwch.",
"dismissable_banner.public_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl ar y we gymdeithasol y mae pobl ar {domain} yn eu dilyn.",
"domain_block_modal.block": "Rhwystro gweinydd",
"domain_block_modal.block_account_instead": "Rhwystro @{name} yn lle hynny",
"domain_block_modal.block": "Blocio gweinydd",
"domain_block_modal.block_account_instead": "Blocio @{name} yn ei le",
"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": "Ni all neb o'r gweinydd hwn eich dilyn.",
"domain_block_modal.they_wont_know": "Fyddan nhw ddim yn gwybod eu bod wedi cael eu rhwystro.",
"domain_block_modal.title": "Rhwystro parth?",
"domain_block_modal.they_wont_know": "Fyddan nhw ddim yn gwybod eu bod wedi cael eu blocio.",
"domain_block_modal.title": "Blocio parth?",
"domain_block_modal.you_will_lose_followers": "Bydd eich holl ddilynwyr o'r gweinydd hwn yn cael eu tynnu.",
"domain_block_modal.you_wont_see_posts": "Fyddwch chi ddim yn gweld postiadau na hysbysiadau gan ddefnyddwyr ar y gweinydd hwn.",
"domain_pill.activitypub_lets_connect": "Mae'n caniatáu ichi gysylltu a rhyngweithio â phobl nid yn unig ar Mastodon, ond ar draws gwahanol apiau cymdeithasol hefyd.",
@ -252,7 +252,7 @@
"empty_column.bookmarked_statuses": "Nid oes gennych unrhyw bostiad wedi'u cadw fel llyfrnodau eto. Pan fyddwch yn gosod nod tudalen i un, mi fydd yn ymddangos yma.",
"empty_column.community": "Mae'r ffrwd lleol yn wag. Beth am ysgrifennu rhywbeth cyhoeddus!",
"empty_column.direct": "Nid oes gennych unrhyw grybwylliadau preifat eto. Pan fyddwch chi'n anfon neu'n derbyn un, bydd yn ymddangos yma.",
"empty_column.domain_blocks": "Nid oes yna unrhyw barthau cuddiedig eto.",
"empty_column.domain_blocks": "Nid oes unrhyw barthau wedi'u blocio eto.",
"empty_column.explore_statuses": "Does dim yn trendio ar hyn o bryd. Dewch nôl nes ymlaen!",
"empty_column.favourited_statuses": "Nid oes gennych unrhyw hoff bostiadau eto. Pan byddwch yn hoffi un, bydd yn ymddangos yma.",
"empty_column.favourites": "Nid oes unrhyw un wedi hoffi'r postiad hwn eto. Pan fydd rhywun yn gwneud hynny, byddan nhw'n ymddangos yma.",
@ -411,6 +411,7 @@
"limited_account_hint.action": "Dangos y proffil beth bynnag",
"limited_account_hint.title": "Mae'r proffil hwn wedi cael ei guddio gan gymedrolwyr {domain}.",
"link_preview.author": "Gan {name}",
"link_preview.more_from_author": "Mwy gan {name}",
"lists.account.add": "Ychwanegu at restr",
"lists.account.remove": "Tynnu o'r rhestr",
"lists.delete": "Dileu rhestr",
@ -480,13 +481,12 @@
"notification.moderation_warning.action_silence": "Mae eich cyfrif wedi'i gyfyngu.",
"notification.moderation_warning.action_suspend": "Mae eich cyfrif wedi'i hatal.",
"notification.own_poll": "Mae eich pleidlais wedi dod i ben",
"notification.poll": "Mae pleidlais rydych wedi pleidleisio ynddi wedi dod i ben",
"notification.reblog": "Hybodd {name} eich post",
"notification.relationships_severance_event": "Wedi colli cysylltiad â {name}",
"notification.relationships_severance_event.account_suspension": "Mae gweinyddwr o {from} wedi atal {target}, sy'n golygu na allwch dderbyn diweddariadau ganddynt mwyach na rhyngweithio â nhw.",
"notification.relationships_severance_event.domain_block": "Mae gweinyddwr o {from} wedi rhwystro {target}, gan gynnwys {followersCount} o'ch dilynwyr a {followingCount, plural, one {# cyfrif} other {# cyfrif}} arall rydych chi'n ei ddilyn.",
"notification.relationships_severance_event.domain_block": "Mae gweinyddwr o {from} wedi blocio {target}, gan gynnwys {followersCount} o'ch dilynwyr a {followingCount, plural, one {# cyfrif} other {# cyfrif}} arall rydych chi'n ei ddilyn.",
"notification.relationships_severance_event.learn_more": "Dysgu mwy",
"notification.relationships_severance_event.user_domain_block": "Rydych wedi rhwystro {target}, gan ddileu {followersCount} o'ch dilynwyr a {followingCount, plural, one {# cyfrif} other {#cyfrifon}} arall rydych yn ei ddilyn.",
"notification.relationships_severance_event.user_domain_block": "Rydych wedi blocio {target}, gan ddileu {followersCount} o'ch dilynwyr a {followingCount, plural, one {# cyfrif} other {#cyfrifon}} arall rydych yn ei ddilyn.",
"notification.status": "{name} newydd ei bostio",
"notification.update": "Golygodd {name} bostiad",
"notification_requests.accept": "Derbyn",
@ -803,7 +803,7 @@
"video.expand": "Ymestyn fideo",
"video.fullscreen": "Sgrin llawn",
"video.hide": "Cuddio fideo",
"video.mute": "Tewi sain",
"video.mute": "Diffodd sain",
"video.pause": "Oedi",
"video.play": "Chwarae",
"video.unmute": "Dad-dewi sain"

View file

@ -37,6 +37,7 @@
"account.followers.empty": "Ingen følger denne bruger endnu.",
"account.followers_counter": "{count, plural, one {{counter} følger} other {{counter} følgere}}",
"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.go_to_profile": "Gå til profil",
"account.hide_reblogs": "Skjul boosts fra @{name}",
@ -62,6 +63,7 @@
"account.requested_follow": "{name} har anmodet 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}}",
"account.unblock": "Afblokér @{name}",
"account.unblock_domain": "Afblokér domænet {domain}",
"account.unblock_short": "Afblokér",
@ -441,6 +443,8 @@
"mute_modal.title": "Tavsgør bruger?",
"mute_modal.you_wont_see_mentions": "Indlæg, som nævner vedkommende, vises ikke.",
"mute_modal.you_wont_see_posts": "Vedkommende kan stadig se dine indlæg, med vedkommendes vise ikke.",
"name_and_others": "{name} og {count, plural, one {# anden} other {# andre}}",
"name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a>",
"navigation_bar.about": "Om",
"navigation_bar.advanced_interface": "Åbn i avanceret webgrænseflade",
"navigation_bar.blocks": "Blokerede brugere",
@ -468,6 +472,10 @@
"navigation_bar.security": "Sikkerhed",
"not_signed_in_indicator.not_signed_in": "Log ind for at tilgå denne ressource.",
"notification.admin.report": "{name} anmeldte {target}",
"notification.admin.report_account": "{name} anmeldte {count, plural, one {et indlæg} other {# indlæg}} fra {target} angående {category}",
"notification.admin.report_account_other": "{name} anmeldte {count, plural, one {et indlæg} other {# indlæg}} fra {target}",
"notification.admin.report_statuses": "{name} anmeldte {target} angående {category}",
"notification.admin.report_statuses_other": "{name} anmeldte {target}",
"notification.admin.sign_up": "{name} tilmeldte sig",
"notification.favourite": "{name} favoritmarkerede dit indlæg",
"notification.follow": "{name} begyndte at følge dig",
@ -483,7 +491,8 @@
"notification.moderation_warning.action_silence": "Din konto er blevet begrænset.",
"notification.moderation_warning.action_suspend": "Din konto er suspenderet.",
"notification.own_poll": "Din afstemning er afsluttet",
"notification.poll": "En afstemning, hvori du stemte, er slut",
"notification.poll": "En afstemning, hvori du har stemt, er slut",
"notification.private_mention": "{name} nævnte dig privat",
"notification.reblog": "{name} boostede dit indlæg",
"notification.relationships_severance_event": "Mistede forbindelser med {name}",
"notification.relationships_severance_event.account_suspension": "En admin fra {from} har suspenderet {target}, hvofor opdateringer herfra eller interaktion hermed ikke længer er mulig.",
@ -501,6 +510,8 @@
"notifications.column_settings.admin.report": "Nye anmeldelser:",
"notifications.column_settings.admin.sign_up": "Nye tilmeldinger:",
"notifications.column_settings.alert": "Computernotifikationer",
"notifications.column_settings.beta.category": "Eksperimentelle funktioner",
"notifications.column_settings.beta.grouping": "Gruppér notifikationer",
"notifications.column_settings.favourite": "Favoritter:",
"notifications.column_settings.filter_bar.advanced": "Vis alle kategorier",
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
@ -664,9 +675,13 @@
"report.unfollow_explanation": "Du følger denne konto. For ikke længere at se vedkommendes indlæg i dit hjemmefeed, kan du stoppe med at følge dem.",
"report_notification.attached_statuses": "{count, plural, one {{count} post} other {{count} poster}} vedhæftet",
"report_notification.categories.legal": "Juridisk",
"report_notification.categories.legal_sentence": "ikke-tilladt indhold",
"report_notification.categories.other": "Andre",
"report_notification.categories.other_sentence": "andet",
"report_notification.categories.spam": "Spam",
"report_notification.categories.spam_sentence": "spam",
"report_notification.categories.violation": "Regelovertrædelse",
"report_notification.categories.violation_sentence": "regelovertrædelse",
"report_notification.open": "Åbn anmeldelse",
"search.no_recent_searches": "Ingen seneste søgninger",
"search.placeholder": "Søg",
@ -694,8 +709,11 @@
"server_banner.about_active_users": "Folk, som brugte denne server de seneste 30 dage (månedlige aktive brugere)",
"server_banner.active_users": "aktive brugere",
"server_banner.administered_by": "Håndteres af:",
"server_banner.is_one_of_many": "{domain} er en af de mange uafhængige Mastodon-servere, man kan bruge for at deltage i fediverset.",
"server_banner.server_stats": "Serverstatstik:",
"sign_in_banner.create_account": "Opret konto",
"sign_in_banner.follow_anyone": "Følg alle på tværs af fediverset og se alt i kronologisk rækkefølge. Ingen algoritmer, annoncer eller clickbait i syne.",
"sign_in_banner.mastodon_is": "Mastodon er den bedste måde at holde sig ajour med, hvad der sker.",
"sign_in_banner.sign_in": "Log ind",
"sign_in_banner.sso_redirect": "Log ind eller Tilmeld",
"status.admin_account": "Åbn modereringsbrugerflade for @{name}",

View file

@ -443,6 +443,8 @@
"mute_modal.title": "Profil stummschalten?",
"mute_modal.you_wont_see_mentions": "Du wirst keine Beiträge sehen, die dieses Profil erwähnen.",
"mute_modal.you_wont_see_posts": "Deine Beiträge können weiterhin angesehen werden, aber du wirst deren Beiträge nicht mehr sehen.",
"name_and_others": "{name} und {count, plural, one {# weitere Person} other {# weitere Personen}}",
"name_and_others_with_link": "{name} und <a>{count, plural, one {# weitere Person} other {# weitere Personen}}</a>",
"navigation_bar.about": "Über",
"navigation_bar.advanced_interface": "Im erweiterten Webinterface öffnen",
"navigation_bar.blocks": "Blockierte Profile",
@ -470,6 +472,10 @@
"navigation_bar.security": "Sicherheit",
"not_signed_in_indicator.not_signed_in": "Du musst dich anmelden, um auf diesen Inhalt zugreifen zu können.",
"notification.admin.report": "{name} meldete {target}",
"notification.admin.report_account": "{name} meldete {count, plural, one {einen Beitrag} other {# Beiträge}} von {target} wegen {category}",
"notification.admin.report_account_other": "{name} meldete {count, plural, one {einen Beitrag} other {# Beiträge}} von {target}",
"notification.admin.report_statuses": "{name} meldete {target} wegen {category}",
"notification.admin.report_statuses_other": "{name} meldete {target}",
"notification.admin.sign_up": "{name} registrierte sich",
"notification.favourite": "{name} favorisierte deinen Beitrag",
"notification.follow": "{name} folgt dir",
@ -486,6 +492,7 @@
"notification.moderation_warning.action_suspend": "Dein Konto wurde gesperrt.",
"notification.own_poll": "Deine Umfrage ist beendet",
"notification.poll": "Eine Umfrage, an der du teilgenommen hast, ist beendet",
"notification.private_mention": "{name} hat dich privat erwähnt",
"notification.reblog": "{name} teilte deinen Beitrag",
"notification.relationships_severance_event": "Verbindungen mit {name} verloren",
"notification.relationships_severance_event.account_suspension": "Ein Admin von {from} hat {target} gesperrt. Du wirst von diesem Profil keine Updates mehr erhalten und auch nicht mit ihm interagieren können.",
@ -503,6 +510,8 @@
"notifications.column_settings.admin.report": "Neue Meldungen:",
"notifications.column_settings.admin.sign_up": "Neue Registrierungen:",
"notifications.column_settings.alert": "Desktop-Benachrichtigungen",
"notifications.column_settings.beta.category": "Experimentelle Funktionen",
"notifications.column_settings.beta.grouping": "Benachrichtigungen gruppieren",
"notifications.column_settings.favourite": "Favoriten:",
"notifications.column_settings.filter_bar.advanced": "Alle Filterkategorien anzeigen",
"notifications.column_settings.filter_bar.category": "Filterleiste",
@ -644,7 +653,7 @@
"report.placeholder": "Ergänzende Hinweise",
"report.reasons.dislike": "Das gefällt mir nicht",
"report.reasons.dislike_description": "Das ist etwas, das du nicht sehen möchtest",
"report.reasons.legal": "Das ist illegal",
"report.reasons.legal": "Das ist rechtswidrig",
"report.reasons.legal_description": "Du glaubst, dass es gegen die Gesetze deines Landes oder des Landes des Servers verstößt",
"report.reasons.other": "Es ist etwas anderes",
"report.reasons.other_description": "Der Vorfall passt zu keiner dieser Kategorien",
@ -666,9 +675,13 @@
"report.unfollow_explanation": "Du folgst diesem Konto. Um die Beiträge nicht mehr auf deiner Startseite zu sehen, entfolge dem Konto.",
"report_notification.attached_statuses": "{count, plural, one {{count} angehangener Beitrag} other {{count} angehängte Beiträge}}",
"report_notification.categories.legal": "Rechtliches",
"report_notification.categories.legal_sentence": "rechtswidrigem Inhalt",
"report_notification.categories.other": "Nicht aufgeführt",
"report_notification.categories.other_sentence": "etwas anderem",
"report_notification.categories.spam": "Spam",
"report_notification.categories.spam_sentence": "Spam",
"report_notification.categories.violation": "Regelverstoß",
"report_notification.categories.violation_sentence": "Regelverletzung",
"report_notification.open": "Meldung öffnen",
"search.no_recent_searches": "Keine früheren Suchanfragen",
"search.placeholder": "Suche",

View file

@ -35,7 +35,9 @@
"account.follow_back": "Ακολούθησε και εσύ",
"account.followers": "Ακόλουθοι",
"account.followers.empty": "Κανείς δεν ακολουθεί αυτόν τον χρήστη ακόμα.",
"account.followers_counter": "{count, plural, one {{counter} ακόλουθος} other {{counter} ακόλουθοι}}",
"account.following": "Ακολουθείτε",
"account.following_counter": "{count, plural, one {{counter} ακολουθεί} other {{counter} ακολουθούν}}",
"account.follows.empty": "Αυτός ο χρήστης δεν ακολουθεί κανέναν ακόμα.",
"account.go_to_profile": "Μετάβαση στο προφίλ",
"account.hide_reblogs": "Απόκρυψη ενισχύσεων από @{name}",
@ -61,6 +63,7 @@
"account.requested_follow": "Ο/Η {name} αιτήθηκε να σε ακολουθήσει",
"account.share": "Κοινοποίηση του προφίλ @{name}",
"account.show_reblogs": "Εμφάνιση ενισχύσεων από @{name}",
"account.statuses_counter": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
"account.unblock": "Άρση αποκλεισμού @{name}",
"account.unblock_domain": "Άρση αποκλεισμού του τομέα {domain}",
"account.unblock_short": "Άρση αποκλεισμού",
@ -75,6 +78,10 @@
"admin.dashboard.retention.average": "Μέσος όρος",
"admin.dashboard.retention.cohort": "Μήνας εγγραφής",
"admin.dashboard.retention.cohort_size": "Νέοι χρήστες",
"admin.impact_report.instance_accounts": "Προφίλ λογαριασμών που θα διαγράψει",
"admin.impact_report.instance_followers": "Ακόλουθοι που θα χάσουν οι χρήστες μας",
"admin.impact_report.instance_follows": "Ακόλουθοι που θα χάσουν οι χρήστες τους",
"admin.impact_report.title": "Περίληψη επιπτώσεων",
"alert.rate_limited.message": "Παρακαλούμε δοκίμασε ξανά μετά τις {retry_time, time, medium}",
"alert.rate_limited.title": "Περιορισμός συχνότητας",
"alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.",
@ -82,6 +89,14 @@
"announcement.announcement": "Ανακοίνωση",
"attachments_list.unprocessed": "(μη επεξεργασμένο)",
"audio.hide": "Απόκρυψη αρχείου ήχου",
"block_modal.remote_users_caveat": "Θα ζητήσουμε από τον διακομιστή {domain} να σεβαστεί την απόφασή σου. Ωστόσο, η συμμόρφωση δεν είναι εγγυημένη δεδομένου ότι ορισμένοι διακομιστές ενδέχεται να χειρίζονται τους αποκλεισμούς διαφορετικά. Οι δημόσιες αναρτήσεις ενδέχεται να είναι ορατές σε μη συνδεδεμένους χρήστες.",
"block_modal.show_less": "Εμφάνιση λιγότερων",
"block_modal.show_more": "Εμφάνιση περισσότερων",
"block_modal.they_cant_mention": "Δεν μπορεί να σε επισημάνει ή να σε ακολουθήσει.",
"block_modal.they_cant_see_posts": "Δεν μπορεί να δει τις αναρτήσεις σου και δε θα δεις τις δικές του.",
"block_modal.they_will_know": "Μπορούν να δει ότι έχει αποκλειστεί.",
"block_modal.title": "Αποκλεισμός χρήστη;",
"block_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον αναφέρουν.",
"boost_modal.combo": "Μπορείς να πατήσεις {combo} για να το προσπεράσεις την επόμενη φορά",
"bundle_column_error.copy_stacktrace": "Αντιγραφή αναφοράς σφάλματος",
"bundle_column_error.error.body": "Δεν ήταν δυνατή η απόδοση της σελίδας που ζήτησες. Μπορεί να οφείλεται σε σφάλμα στον κώδικά μας ή σε πρόβλημα συμβατότητας του προγράμματος περιήγησης.",
@ -108,6 +123,7 @@
"column.directory": "Περιήγηση στα προφίλ",
"column.domain_blocks": "Αποκλεισμένοι τομείς",
"column.favourites": "Αγαπημένα",
"column.firehose": "Ζωντανές ροές",
"column.follow_requests": "Αιτήματα ακολούθησης",
"column.home": "Αρχική",
"column.lists": "Λίστες",
@ -140,6 +156,7 @@
"compose_form.poll.duration": "Διάρκεια δημοσκόπησης",
"compose_form.poll.multiple": "Πολλαπλή επιλογή",
"compose_form.poll.option_placeholder": "Επιλογή {number}",
"compose_form.poll.single": "Διάλεξε ένα",
"compose_form.poll.switch_to_multiple": "Ενημέρωση δημοσκόπησης με πολλαπλές επιλογές",
"compose_form.poll.switch_to_single": "Ενημέρωση δημοσκόπησης με μοναδική επιλογή",
"compose_form.poll.type": "Στυλ",
@ -160,6 +177,7 @@
"confirmations.delete_list.message": "Σίγουρα θες να διαγράψεις οριστικά αυτή τη λίστα;",
"confirmations.discard_edit_media.confirm": "Απόρριψη",
"confirmations.discard_edit_media.message": "Έχεις μη αποθηκευμένες αλλαγές στην περιγραφή πολυμέσων ή στην προεπισκόπηση, απόρριψη ούτως ή άλλως;",
"confirmations.domain_block.confirm": "Αποκλεισμός διακομιστή",
"confirmations.domain_block.message": "Σίγουρα θες να αποκλείσεις ολόκληρο τον {domain}; Συνήθως μερικοί συγκεκρίμένοι αποκλεισμοί ή σιγάσεις επαρκούν και προτιμούνται. Δεν θα βλέπεις περιεχόμενο από αυτό τον τομέα σε καμία δημόσια ροή ή στις ειδοποιήσεις σου. Όσους ακόλουθους έχεις αυτό αυτό τον τομέα θα αφαιρεθούν.",
"confirmations.edit.confirm": "Επεξεργασία",
"confirmations.edit.message": "Αν το επεξεργαστείς τώρα θα αντικατασταθεί το μήνυμα που συνθέτεις. Είσαι σίγουρος ότι θέλεις να συνεχίσεις;",
@ -190,6 +208,28 @@
"dismissable_banner.explore_links": "Αυτές οι ειδήσεις συζητούνται σε αυτόν και άλλους διακομιστές του αποκεντρωμένου δικτύου αυτή τη στιγμή.",
"dismissable_banner.explore_statuses": "Αυτές είναι οι αναρτήσεις που έχουν απήχηση στο κοινωνικό δίκτυο σήμερα. Οι νεώτερες αναρτήσεις με περισσότερες προωθήσεις και προτιμήσεις κατατάσσονται ψηλότερα.",
"dismissable_banner.explore_tags": "Αυτές οι ετικέτες αποκτούν απήχηση σε αυτόν και άλλους διακομιστές του αποκεντρωμένου δικτύου αυτή τη στιγμή.",
"dismissable_banner.public_timeline": "Αυτές είναι οι πιο πρόσφατες δημόσιες αναρτήσεις από άτομα στον κοινωνικό ιστό που ακολουθούν άτομα από το {domain}.",
"domain_block_modal.block": "Αποκλεισμός διακομιστή",
"domain_block_modal.block_account_instead": "Αποκλεισμός @{name} αντ' αυτού",
"domain_block_modal.they_can_interact_with_old_posts": "Άτομα από αυτόν τον διακομιστή μπορούν να αλληλεπιδράσουν με τις παλιές αναρτήσεις σου.",
"domain_block_modal.they_cant_follow": "Κανείς από αυτόν τον διακομιστή δεν μπορεί να σε ακολουθήσει.",
"domain_block_modal.they_wont_know": "Δεν θα ξέρουν ότι έχουν αποκλειστεί.",
"domain_block_modal.title": "Αποκλεισμός τομέα;",
"domain_block_modal.you_will_lose_followers": "Οι ακόλουθοί σου από αυτόν τον διακομιστή θα αφαιρεθούν.",
"domain_block_modal.you_wont_see_posts": "Δεν θα βλέπεις αναρτήσεις ή ειδοποιήσεις από χρήστες σε αυτόν το διακομιστή.",
"domain_pill.activitypub_lets_connect": "Σού επιτρέπει να συνδεθείς και να αλληλεπιδράσεις με τους ανθρώπους όχι μόνο στο Mastodon, αλλά και σε διαφορετικές κοινωνικές εφαρμογές.",
"domain_pill.activitypub_like_language": "Το ActivityPub είναι σαν τη γλώσσα Mastodon μιλάει με άλλα κοινωνικά δίκτυα.",
"domain_pill.server": "Διακομιστής",
"domain_pill.their_handle": "Το πλήρες όνομα χρήστη:",
"domain_pill.their_server": "Το ψηφιακό του σπίτι, όπου ζουν όλες οι αναρτήσεις του.",
"domain_pill.their_username": "Το μοναδικό του αναγνωριστικό στο διακομιστή του. Είναι πιθανό να βρεις χρήστες με το ίδιο όνομα χρήστη σε διαφορετικούς διακομιστές.",
"domain_pill.username": "Όνομα χρήστη",
"domain_pill.whats_in_a_handle": "Τί υπάρχει σε ένα πλήρες όνομα χρήστη;",
"domain_pill.who_they_are": "Από τη στιγμή που τα πλήρη ονόματα λένε ποιος είναι κάποιος και πού είναι, μπορείς να αλληλεπιδράσεις με άτομα απ' όλο τον κοινωνικό ιστό των <button> πλατφορμών που στηρίζονται στο ActivityPub</button>.",
"domain_pill.who_you_are": "Επειδή το πλήρες όνομα χρήστη σου λέει ποιος είσαι και πού βρίσκεσαι, άτομα μπορούν να αλληλεπιδράσουν μαζί σου στον κοινωνικό ιστό των <button>πλατφορμών που στηρίζονται στο ActivityPub</button>.",
"domain_pill.your_handle": "Το πλήρες όνομα χρήστη σου:",
"domain_pill.your_server": "Το ψηφιακό σου σπίτι, όπου ζουν όλες σου οι αναρτήσεις. Δε σ' αρέσει αυτός; Μετακινήσου σε διακομιστές ανά πάσα στιγμή και πάρε και τους ακόλουθούς σου.",
"domain_pill.your_username": "Το μοναδικό σου αναγνωριστικό σε τούτο τον διακομιστή. Είναι πιθανό να βρεις χρήστες με το ίδιο όνομα χρήστη σε διαφορετικούς διακομιστές.",
"embed.instructions": "Ενσωμάτωσε αυτή την ανάρτηση στην ιστοσελίδα σου αντιγράφοντας τον παρακάτω κώδικα.",
"embed.preview": "Ορίστε πως θα φαίνεται:",
"emoji_button.activity": "Δραστηριότητα",
@ -207,6 +247,7 @@
"emoji_button.search_results": "Αποτελέσματα αναζήτησης",
"emoji_button.symbols": "Σύμβολα",
"emoji_button.travel": "Ταξίδια & Τοποθεσίες",
"empty_column.account_hides_collections": "Αυτός ο χρήστης έχει επιλέξει να μην καταστήσει αυτές τις πληροφορίες διαθέσιμες",
"empty_column.account_suspended": "Λογαριασμός σε αναστολή",
"empty_column.account_timeline": "Δεν έχει αναρτήσεις εδώ!",
"empty_column.account_unavailable": "Μη διαθέσιμο προφίλ",
@ -216,6 +257,8 @@
"empty_column.direct": "Δεν έχεις καμία προσωπική επισήμανση ακόμα. Όταν στείλεις ή λάβεις μία, θα εμφανιστεί εδώ.",
"empty_column.domain_blocks": "Δεν υπάρχουν αποκλεισμένοι τομείς ακόμα.",
"empty_column.explore_statuses": "Τίποτα δεν βρίσκεται στις τάσεις αυτή τη στιγμή. Έλεγξε αργότερα!",
"empty_column.favourited_statuses": "Δεν έχεις καμία αγαπημένη ανάρτηση ακόμα. Μόλις αγαπήσεις κάποια, θα εμφανιστεί εδώ.",
"empty_column.favourites": "Κανείς δεν έχει αγαπήσει αυτή την ανάρτηση ακόμα. Μόλις το κάνει κάποιος, θα εμφανιστεί εδώ.",
"empty_column.follow_requests": "Δεν έχεις κανένα αίτημα παρακολούθησης ακόμα. Μόλις λάβεις κάποιο, θα εμφανιστεί εδώ.",
"empty_column.followed_tags": "Δεν έχετε παρακολουθήσει ακόμα καμία ετικέτα. Όταν το κάνετε, θα εμφανιστούν εδώ.",
"empty_column.hashtag": "Δεν υπάρχει ακόμα κάτι για αυτή την ετικέτα.",
@ -223,6 +266,7 @@
"empty_column.list": "Δεν υπάρχει τίποτα σε αυτή τη λίστα ακόμα. Όταν τα μέλη της δημοσιεύσουν νέες καταστάσεις, θα εμφανιστούν εδώ.",
"empty_column.lists": "Δεν έχεις καμία λίστα ακόμα. Μόλις φτιάξεις μια, θα εμφανιστεί εδώ.",
"empty_column.mutes": "Δεν έχεις κανένα χρήστη σε σίγαση ακόμα.",
"empty_column.notification_requests": "Όλα καθαρά! Δεν υπάρχει τίποτα εδώ. Όταν λαμβάνεις νέες ειδοποιήσεις, αυτές θα εμφανίζονται εδώ σύμφωνα με τις ρυθμίσεις σου.",
"empty_column.notifications": "Δεν έχεις ειδοποιήσεις ακόμα. Όταν άλλα άτομα αλληλεπιδράσουν μαζί σου, θα το δεις εδώ.",
"empty_column.public": "Δεν υπάρχει τίποτα εδώ! Γράψε κάτι δημόσιο ή ακολούθησε χειροκίνητα χρήστες από άλλους διακομιστές για να τη γεμίσεις",
"error.unexpected_crash.explanation": "Είτε λόγω σφάλματος στον κώδικά μας ή λόγω ασυμβατότητας με τον περιηγητή, η σελίδα δε μπόρεσε να εμφανιστεί σωστά.",
@ -253,12 +297,30 @@
"filter_modal.select_filter.subtitle": "Χρησιμοποιήστε μια υπάρχουσα κατηγορία ή δημιουργήστε μια νέα",
"filter_modal.select_filter.title": "Φιλτράρισμα αυτής της ανάρτησης",
"filter_modal.title.status": "Φιλτράρισμα μιας ανάρτησης",
"filtered_notifications_banner.mentions": "{count, plural, one {επισήμανση} other {επισημάνσεις}}",
"filtered_notifications_banner.pending_requests": "Ειδοποιήσεις από {count, plural, =0 {κανένα} one {ένα άτομο} other {# άτομα}} που μπορεί να ξέρεις",
"filtered_notifications_banner.title": "Φιλτραρισμένες ειδοποιήσεις",
"firehose.all": "Όλα",
"firehose.local": "Αυτός ο διακομιστής",
"firehose.remote": "Άλλοι διακομιστές",
"follow_request.authorize": "Εξουσιοδότησε",
"follow_request.reject": "Απέρριψε",
"follow_requests.unlocked_explanation": "Παρόλο που ο λογαριασμός σου δεν είναι κλειδωμένος, το προσωπικό του {domain} θεώρησαν πως ίσως να θέλεις να ελέγξεις χειροκίνητα αυτά τα αιτήματα ακολούθησης.",
"follow_suggestions.curated_suggestion": "Επιλογή προσωπικού",
"follow_suggestions.dismiss": "Να μην εμφανιστεί ξανά",
"follow_suggestions.featured_longer": "Προσεκτικά επιλεγμένα απ' την ομάδα του {domain}",
"follow_suggestions.friends_of_friends_longer": "Δημοφιλή μεταξύ των ατόμων που ακολουθείς",
"follow_suggestions.hints.featured": "Αυτό το προφίλ έχει επιλεγεί προσεκτικά από την ομάδα του {domain}.",
"follow_suggestions.hints.friends_of_friends": "Αυτό το προφίλ είναι δημοφιλές μεταξύ των ατόμων που ακολουθείς.",
"follow_suggestions.hints.most_followed": "Αυτό το προφίλ είναι ένα από τα πιο ακολουθούμενα στο {domain}.",
"follow_suggestions.hints.most_interactions": "Αυτό το προφίλ έχει πάρει πρόσφατα μεγάλη προσοχή στο {domain}.",
"follow_suggestions.hints.similar_to_recently_followed": "Αυτό το προφίλ είναι παρόμοιο με τα προφίλ που έχεις ακολουθήσει πρόσφατα.",
"follow_suggestions.personalized_suggestion": "Εξατομικευμένη πρόταση",
"follow_suggestions.popular_suggestion": "Δημοφιλής πρόταση",
"follow_suggestions.popular_suggestion_longer": "Δημοφιλή στο {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Παρόμοια με προφίλ που ακολούθησες πρόσφατα",
"follow_suggestions.view_all": "Εμφάνιση όλων",
"follow_suggestions.who_to_follow": "Ποιον να ακολουθήσεις",
"followed_tags": "Ετικέτες που ακολουθούνται",
"footer.about": "Σχετικά με",
"footer.directory": "Κατάλογος προφίλ",
@ -279,21 +341,30 @@
"hashtag.column_settings.tag_mode.any": "Οποιοδήποτε από αυτά",
"hashtag.column_settings.tag_mode.none": "Κανένα από αυτά",
"hashtag.column_settings.tag_toggle": "Προσθήκη επιπλέον ταμπελών για την κολώνα",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} συμμετέχων} other {{counter} συμμετέχοντες}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}} σήμερα",
"hashtag.follow": "Παρακολούθηση ετικέτας",
"hashtag.unfollow": "Διακοπή παρακολούθησης ετικέτας",
"hashtags.and_other": "…και {count, plural, one {}other {# ακόμη}}",
"home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων",
"home.column_settings.show_replies": "Εμφάνιση απαντήσεων",
"home.hide_announcements": "Απόκρυψη ανακοινώσεων",
"home.pending_critical_update.body": "Παρακαλούμε ενημέρωσε τον διακομιστή Mastodon σου το συντομότερο δυνατόν!",
"home.pending_critical_update.link": "Δείτε ενημερώσεις",
"home.pending_critical_update.title": "Κρίσιμη ενημέρωση ασφαλείας διαθέσιμη!",
"home.show_announcements": "Εμφάνιση ανακοινώσεων",
"interaction_modal.description.favourite": "Με ένα συντάκτη στο Mastodon μπορείς να αγαπήσεις αυτή την ανάρτηση, για να ενημερώσεις τον συγγραφέα ότι την εκτιμάς και να την αποθηκεύσεις για αργότερα.",
"interaction_modal.description.follow": "Με έναν λογαριασμό Mastodon, μπορείς να ακολουθήσεις τον/την {name} ώστε να λαμβάνεις τις αναρτήσεις του/της στη δική σου ροή.",
"interaction_modal.description.reblog": "Με ένα λογαριασμό Mastodon, μπορείς να ενισχύσεις αυτή την ανάρτηση για να τη μοιραστείς με τους δικούς σου ακολούθους.",
"interaction_modal.description.reply": "Με ένα λογαριασμό Mastodon, μπορείς να απαντήσεις σε αυτή την ανάρτηση.",
"interaction_modal.login.action": "Take me home\nΠήγαινέ με στην αρχική σελίδα",
"interaction_modal.login.prompt": "Τομέας του οικιακού σου διακομιστή, πχ. mastodon.social",
"interaction_modal.no_account_yet": "Not on Mastodon?\nΔεν είστε στο Mastodon;",
"interaction_modal.on_another_server": "Σε διαφορετικό διακομιστή",
"interaction_modal.on_this_server": "Σε αυτόν τον διακομιστή",
"interaction_modal.sign_in": "Δεν είσαι συνδεδεμένος σε αυτόν το διακομιστή. Πού φιλοξενείται ο λογαριασμός σου;",
"interaction_modal.sign_in_hint": "Συμβουλή: Αυτή είναι η ιστοσελίδα όπου έχεις εγγραφεί. Αν δεν θυμάσαι, αναζήτησε το καλώς ήρθες e-mail στα εισερχόμενά σου. Μπορείς επίσης να εισάγεις το πλήρες όνομα χρήστη! (πχ. @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Favorite {name}'s post\nΠροτίμησε την ανάρτηση της/του {name}",
"interaction_modal.title.follow": "Ακολούθησε {name}",
"interaction_modal.title.reblog": "Ενίσχυσε την ανάρτηση του {name}",
@ -311,6 +382,7 @@
"keyboard_shortcuts.down": "κίνηση προς τα κάτω στη λίστα",
"keyboard_shortcuts.enter": "Εμφάνιση ανάρτησης",
"keyboard_shortcuts.favourite": "Αγαπημένη δημοσίευση",
"keyboard_shortcuts.favourites": "Άνοιγμα λίστας αγαπημένων",
"keyboard_shortcuts.federated": "Άνοιγμα ροής συναλλαγών",
"keyboard_shortcuts.heading": "Συντομεύσεις πληκτρολογίου",
"keyboard_shortcuts.home": "Άνοιγμα ροής αρχικής σελίδας",
@ -341,11 +413,15 @@
"lightbox.previous": "Προηγούμενο",
"limited_account_hint.action": "Εμφάνιση προφίλ ούτως ή άλλως",
"limited_account_hint.title": "Αυτό το προφίλ έχει αποκρυφτεί από τους διαχειριστές του διακομιστή {domain}.",
"link_preview.author": "Από {name}",
"link_preview.more_from_author": "Περισσότερα από {name}",
"link_preview.shares": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
"lists.account.add": "Πρόσθεσε στη λίστα",
"lists.account.remove": "Βγάλε από τη λίστα",
"lists.delete": "Διαγραφή λίστας",
"lists.edit": "Επεξεργασία λίστας",
"lists.edit.submit": "Αλλαγή τίτλου",
"lists.exclusive": "Απόκρυψη αυτών των αναρτήσεων από την αρχική",
"lists.new.create": "Προσθήκη λίστας",
"lists.new.title_placeholder": "Τίτλος νέας λίστα",
"lists.replies_policy.followed": "Οποιοσδήποτε χρήστης που ακολουθείς",
@ -358,7 +434,19 @@
"loading_indicator.label": "Φόρτωση…",
"media_gallery.toggle_visible": "{number, plural, one {Απόκρυψη εικόνας} other {Απόκρυψη εικόνων}}",
"moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.",
"mute_modal.hide_from_notifications": "Απόκρυψη από ειδοποιήσεις",
"mute_modal.hide_options": "Απόκρυψη επιλογών",
"mute_modal.indefinite": "Μέχρι να κάνω άρση σίγασης",
"mute_modal.show_options": "Εμφάνιση επιλογών",
"mute_modal.they_can_mention_and_follow": "Μπορεί να σε αναφέρει και να σε ακολουθήσει, αλλά δε θα τον βλέπεις.",
"mute_modal.they_wont_know": "Δε θα ξέρει ότι είναι σε σίγαση.",
"mute_modal.title": "Σίγαση χρήστη;",
"mute_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον αναφέρουν.",
"mute_modal.you_wont_see_posts": "Μπορεί ακόμα να δει τις αναρτήσεις σου, αλλά δε θα βλέπεις τις δικές του.",
"name_and_others": "{name} και {count, plural, one {# ακόμη} other {# ακόμη}}",
"name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a>",
"navigation_bar.about": "Σχετικά με",
"navigation_bar.advanced_interface": "Άνοιγμα σε προηγμένη διεπαφή ιστού",
"navigation_bar.blocks": "Αποκλεισμένοι χρήστες",
"navigation_bar.bookmarks": "Σελιδοδείκτες",
"navigation_bar.community_timeline": "Τοπική ροή",
@ -367,6 +455,7 @@
"navigation_bar.discover": "Ανακάλυψη",
"navigation_bar.domain_blocks": "Αποκλεισμένοι τομείς",
"navigation_bar.explore": "Εξερεύνηση",
"navigation_bar.favourites": "Αγαπημένα",
"navigation_bar.filters": "Αποσιωπημένες λέξεις",
"navigation_bar.follow_requests": "Αιτήματα ακολούθησης",
"navigation_bar.followed_tags": "Ετικέτες που ακολουθούνται",
@ -383,22 +472,49 @@
"navigation_bar.security": "Ασφάλεια",
"not_signed_in_indicator.not_signed_in": "Πρέπει να συνδεθείς για να αποκτήσεις πρόσβαση σε αυτόν τον πόρο.",
"notification.admin.report": "Ο/Η {name} ανέφερε τον {target}",
"notification.admin.report_account": "Ο χρήστης {name} ανέφερε {count, plural, one {μία ανάρτηση} other {# αναρτήσεις}} από {target} για {category}",
"notification.admin.report_account_other": "Ο χρήστης {name} ανέφερε {count, plural, one {μία ανάρτηση} other {# αναρτήσεις}} από {target}",
"notification.admin.report_statuses": "Ο χρήστης {name} ανέφερε τον χρήστη {target} για {category}",
"notification.admin.report_statuses_other": "Ο χρήστης {name} ανέφερε τον χρήστη {target}",
"notification.admin.sign_up": "{name} έχει εγγραφεί",
"notification.favourite": "{name} favorited your post\n{name} προτίμησε την ανάρτηση σου",
"notification.follow": "Ο/Η {name} σε ακολούθησε",
"notification.follow_request": "Ο/H {name} ζήτησε να σε ακολουθήσει",
"notification.mention": "Ο/Η {name} σε επισήμανε",
"notification.moderation-warning.learn_more": "Μάθε περισσότερα",
"notification.moderation_warning": "Έχετε λάβει μία προειδοποίηση συντονισμού",
"notification.moderation_warning.action_delete_statuses": "Ορισμένες από τις αναρτήσεις σου έχουν αφαιρεθεί.",
"notification.moderation_warning.action_disable": "Ο λογαριασμός σου έχει απενεργοποιηθεί.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Μερικές από τις αναρτήσεις σου έχουν επισημανθεί ως ευαίσθητες.",
"notification.moderation_warning.action_none": "Ο λογαριασμός σου έχει λάβει προειδοποίηση συντονισμού.",
"notification.moderation_warning.action_sensitive": "Οι αναρτήσεις σου θα επισημαίνονται, από εδώ και στο εξής, ως ευαίσθητες.",
"notification.moderation_warning.action_silence": "Ο λογαριασμός σου έχει περιοριστεί.",
"notification.moderation_warning.action_suspend": "Ο λογαριασμός σου έχει ανασταλεί.",
"notification.own_poll": "Η δημοσκόπησή σου έληξε",
"notification.poll": "Τελείωσε μια από τις δημοσκοπήσεις που συμμετείχες",
"notification.poll": "Μία ψηφοφορία στην οποία συμμετείχες έχει τελειώσει",
"notification.private_mention": "{name} σέ επισήμανε ιδιωτικά",
"notification.reblog": "Ο/Η {name} ενίσχυσε τη δημοσίευσή σου",
"notification.relationships_severance_event": "Χάθηκε η σύνδεση με το {name}",
"notification.relationships_severance_event.account_suspension": "Ένας διαχειριστής από το {from} ανέστειλε το {target}, πράγμα που σημαίνει ότι δεν μπορείς πλέον να λαμβάνεις ενημερώσεις από αυτούς ή να αλληλεπιδράς μαζί τους.",
"notification.relationships_severance_event.domain_block": "Ένας διαχειριστής από {from} έχει μπλοκάρει το {target}, συμπεριλαμβανομένων {followersCount} από τους ακόλουθούς σου και {followingCount, plural, one {# λογαριασμό} other {# λογαριασμοί}} που ακολουθείς.",
"notification.relationships_severance_event.learn_more": "Μάθε περισσότερα",
"notification.relationships_severance_event.user_domain_block": "Έχεις αποκλείσει τον λογαριασμό {target}, αφαιρώντας {followersCount} από τους ακόλουθούς σου και {followingCount, plural, one {# λογαριασμό} other {# λογαριασμοί}} που ακολουθείς.",
"notification.status": "Ο/Η {name} μόλις ανέρτησε κάτι",
"notification.update": "ο/η {name} επεξεργάστηκε μια ανάρτηση",
"notification_requests.accept": "Αποδοχή",
"notification_requests.dismiss": "Απόρριψη",
"notification_requests.notifications_from": "Ειδοποιήσεις από {name}",
"notification_requests.title": "Φιλτραρισμένες ειδοποιήσεις",
"notifications.clear": "Καθαρισμός ειδοποιήσεων",
"notifications.clear_confirmation": "Σίγουρα θέλεις να καθαρίσεις μόνιμα όλες τις ειδοποιήσεις σου;",
"notifications.column_settings.admin.report": "Νέες αναφορές:",
"notifications.column_settings.admin.sign_up": "Νέες εγγραφές:",
"notifications.column_settings.alert": "Ειδοποιήσεις επιφάνειας εργασίας",
"notifications.column_settings.beta.category": "Πειραματικές λειτουργίες",
"notifications.column_settings.beta.grouping": "Ομαδοποίηση ειδοποιήσεων",
"notifications.column_settings.favourite": "Αγαπημένα:",
"notifications.column_settings.filter_bar.advanced": "Εμφάνιση όλων των κατηγοριών",
"notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου",
"notifications.column_settings.follow": "Νέοι ακόλουθοι:",
"notifications.column_settings.follow_request": "Νέο αίτημα ακολούθησης:",
"notifications.column_settings.mention": "Επισημάνσεις:",
@ -413,6 +529,7 @@
"notifications.column_settings.update": "Επεξεργασίες:",
"notifications.filter.all": "Όλες",
"notifications.filter.boosts": "Προωθήσεις",
"notifications.filter.favourites": "Αγαπημένα",
"notifications.filter.follows": "Ακολουθείς",
"notifications.filter.mentions": "Επισημάνσεις",
"notifications.filter.polls": "Αποτελέσματα δημοσκόπησης",
@ -423,6 +540,15 @@
"notifications.permission_denied": "Οι ειδοποιήσεις στην επιφάνεια εργασίας δεν είναι διαθέσιμες διότι έχει απορριφθεί κάποιο προηγούμενο αίτημα άδειας",
"notifications.permission_denied_alert": "Δεν είναι δυνατή η ενεργοποίηση των ειδοποιήσεων της επιφάνειας εργασίας, καθώς η άδεια του προγράμματος περιήγησης έχει απορριφθεί νωρίτερα",
"notifications.permission_required": "Οι ειδοποιήσεις δεν είναι διαθέσιμες επειδή δεν έχει δοθεί η απαιτούμενη άδεια.",
"notifications.policy.filter_new_accounts.hint": "Δημιουργήθηκε εντός {days, plural, one {της τελευταίας ημέρας} other {των τελευταίων # ημερών}}",
"notifications.policy.filter_new_accounts_title": "Νέοι λογαριασμοί",
"notifications.policy.filter_not_followers_hint": "Συμπεριλαμβανομένων των ατόμων που σας έχουν ακολουθήσει λιγότερο από {days, plural, one {μια ημέρα} other {# ημέρες}} πριν",
"notifications.policy.filter_not_followers_title": "Άτομα που δε σε ακολουθούν",
"notifications.policy.filter_not_following_hint": "Μέχρι να τους εγκρίνεις χειροκίνητα",
"notifications.policy.filter_not_following_title": "Άτομα που δεν ακολουθείς",
"notifications.policy.filter_private_mentions_hint": "Φιλτραρισμένο εκτός αν είναι απάντηση σε δική σου αναφορά ή αν ακολουθείς τον αποστολέα",
"notifications.policy.filter_private_mentions_title": "Μη συναινετικές ιδιωτικές αναφορές",
"notifications.policy.title": "Φιλτράρισμα ειδοποιήσεων από…",
"notifications_permission_banner.enable": "Ενεργοποίηση ειδοποιήσεων επιφάνειας εργασίας",
"notifications_permission_banner.how_to_control": "Για να λαμβάνεις ειδοποιήσεις όταν το Mastodon δεν είναι ανοιχτό, ενεργοποίησε τις ειδοποιήσεις επιφάνειας εργασίας. Μπορείς να ελέγξεις με ακρίβεια ποιοι τύποι αλληλεπιδράσεων δημιουργούν ειδοποιήσεις επιφάνειας εργασίας μέσω του κουμπιού {icon} μόλις ενεργοποιηθούν.",
"notifications_permission_banner.title": "Μη χάσεις στιγμή",
@ -430,8 +556,15 @@
"onboarding.actions.back": "Επιστροφή",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Πηγαίνετε στην αρχική σας ροή",
"onboarding.compose.template": "Γειά σου #Mastodon!",
"onboarding.follows.empty": "Δυστυχώς, δεν μπορούν να εμφανιστούν αποτελέσματα αυτή τη στιγμή. Μπορείς να προσπαθήσεις να χρησιμοποιήσεις την αναζήτηση ή να περιηγηθείς στη σελίδα εξερεύνησης για να βρεις άτομα να ακολουθήσεις ή να δοκιμάσεις ξανά αργότερα.",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Δημοφιλή στο Mastodon",
"onboarding.profile.discoverable": "Κάνε το προφίλ μου ανακαλύψιμο",
"onboarding.profile.discoverable_hint": "Όταν επιλέγεις την δυνατότητα ανακάλυψης στο Mastodon, οι αναρτήσεις σου μπορεί να εμφανιστούν στα αποτελέσματα αναζήτησης και τις τάσεις, και το προφίλ σου μπορεί να προτείνεται σε άτομα με παρόμοια ενδιαφέροντα με εσένα.",
"onboarding.profile.display_name": "Εμφανιζόμενο όνομα",
"onboarding.profile.display_name_hint": "Το πλήρες ή το διασκεδαστικό σου όνομα…",
"onboarding.profile.lead": "Μπορείς πάντα να το ολοκληρώσεις αργότερα στις ρυθμίσεις, όπου είναι διαθέσιμες ακόμα περισσότερες επιλογές προσαρμογής.",
"onboarding.profile.note": "Βιογραφικό",
"onboarding.profile.note_hint": "Μπορείτε να @αναφέρετε άλλα άτομα ή #hashtags…",
"onboarding.profile.save_and_continue": "Αποθήκευση και συνέχεια",
@ -439,7 +572,9 @@
"onboarding.profile.upload_avatar": "Μεταφόρτωση εικόνας προφίλ",
"onboarding.profile.upload_header": "Μεταφόρτωση κεφαλίδας προφίλ",
"onboarding.share.lead": "Let people know how they can find you on Mastodon!\nΕνημερώστε άλλα άτομα πώς μπορούν να σας βρουν στο Mastodon!",
"onboarding.share.message": "Με λένε {username} στο #Mastodon! Έλα να με ακολουθήσεις στο {url}",
"onboarding.share.next_steps": "Πιθανά επόμενα βήματα:",
"onboarding.share.title": "Κοινοποίηση του προφίλ σου",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.title": "You've made it!\nΤα καταφέρατε!",
@ -451,6 +586,10 @@
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"onboarding.tips.2fa": "<strong>Το ήξερες;</strong> Μπορείς να ασφαλίσεις το λογαριασμό σου ρυθμίζοντας ταυτότητα δύο παραγόντων στις ρυθμίσεις του λογαριασμού σου. Λειτουργεί με οποιαδήποτε εφαρμογή TOTP της επιλογής σας, δεν απαιτείται αριθμός τηλεφώνου!",
"onboarding.tips.accounts_from_other_servers": "<strong>Το ήξερες;</strong> Από τη στιγμή που το Mastodon είναι αποκεντρωμένο, κάποια προφίλ που συναντάς θα φιλοξενούνται σε διακομιστές διαφορετικούς από τον δικό σου. Και παρόλα αυτά μπορείς να αλληλεπιδράσεις μαζί τους απρόσκοπτα! Ο διακομιστής τους είναι στο δεύτερο μισό του ονόματος χρήστη!",
"onboarding.tips.migration": "<strong>Το ήξερες;</strong> Αν αισθάνεσαι ότι το {domain} δεν είναι η κατάλληλη επιλογή διακομιστή για σένα στο μέλλον, μπορείς να μετακινηθείς σε άλλο διακομιστή Mastodon χωρίς να χάσεις τους ακόλουθούς σου. Μπορείς να κάνεις ακόμα και τον δικό σου διακομιστή!",
"onboarding.tips.verification": "<strong>Το ήξερες;</strong> Μπορείς να επαληθεύσεις τον λογαριασμό σου βάζοντας έναν σύνδεσμο του προφίλ σου στο Mastodon στην ιστοσελίδα σου και να προσθέσεις την ιστοσελίδα στο προφίλ σου. Χωρίς έξοδα ή έγγραφα!",
"password_confirmation.exceeds_maxlength": "Η επιβεβαίωση κωδικού πρόσβασης υπερβαίνει το μέγιστο μήκος κωδικού πρόσβασης",
"password_confirmation.mismatching": "Η επιβεβαίωση του κωδικού πρόσβασης δε συμπίπτει",
"picture_in_picture.restore": "Βάλε το πίσω",
@ -469,7 +608,11 @@
"privacy.direct.short": "Συγκεκριμένα άτομα",
"privacy.private.long": "Μόνο οι ακόλουθοί σας",
"privacy.private.short": "Ακόλουθοι",
"privacy.public.long": "Όλοι εντός και εκτός του Mastodon",
"privacy.public.short": "Δημόσιο",
"privacy.unlisted.additional": "Αυτό συμπεριφέρεται ακριβώς όπως το δημόσιο, εκτός από το ότι η ανάρτηση δεν θα εμφανιστεί σε ζωντανές ροές ή ετικέτες, εξερεύνηση ή αναζήτηση στο Mastodon, ακόμη και αν το έχεις επιλέξει για τον λογαριασμό σου.",
"privacy.unlisted.long": "Λιγότερα αλγοριθμικά κόλπα",
"privacy.unlisted.short": "Ήσυχα δημόσια",
"privacy_policy.last_updated": "Τελευταία ενημέρωση {date}",
"privacy_policy.title": "Πολιτική Απορρήτου",
"recommended": "Προτεινόμενα",
@ -487,6 +630,7 @@
"relative_time.minutes": "{number}λ",
"relative_time.seconds": "{number}δ",
"relative_time.today": "σήμερα",
"reply_indicator.attachments": "{count, plural, one {# συνημμένο} other {# συνημμένα}}",
"reply_indicator.cancel": "Άκυρο",
"reply_indicator.poll": "Δημοσκόπηση",
"report.block": "Αποκλεισμός",
@ -530,9 +674,14 @@
"report.unfollow": "Κατάργηση ακολούθησης του @{name}",
"report.unfollow_explanation": "Ακολουθείς αυτό τον λογαριασμό. Για να μη βλέπεις τις αναρτήσεις τους στη δική σου ροή, πάψε να τον ακολουθείς.",
"report_notification.attached_statuses": "{count, plural, one {{count} ανάρτηση} other {{count} αναρτήσεις}} επισυνάπτονται",
"report_notification.categories.legal": "Νομικά",
"report_notification.categories.legal_sentence": "παράνομο περιεχόμενο",
"report_notification.categories.other": "Άλλες",
"report_notification.categories.other_sentence": "άλλο",
"report_notification.categories.spam": "Ανεπιθύμητα",
"report_notification.categories.spam_sentence": "ανεπιθύμητα",
"report_notification.categories.violation": "Παραβίαση κανόνα",
"report_notification.categories.violation_sentence": "παραβίαση κανόνα",
"report_notification.open": "Ανοιχτή αναφορά",
"search.no_recent_searches": "Καμία πρόσφατη αναζήτηση",
"search.placeholder": "Αναζήτηση",
@ -542,8 +691,13 @@
"search.quick_action.open_url": "Άνοιγμα διεύθυνσης URL στο Mastodon",
"search.quick_action.status_search": "Αναρτήσεις που ταιριάζουν με {x}",
"search.search_or_paste": "Αναζήτηση ή εισαγωγή URL",
"search_popout.full_text_search_disabled_message": "Μη διαθέσιμο στο {domain}.",
"search_popout.full_text_search_logged_out_message": "Διαθέσιμο μόνο όταν συνδεθείς.",
"search_popout.language_code": "Κωδικός γλώσσας ISO",
"search_popout.options": "Επιλογές αναζήτησης",
"search_popout.quick_actions": "Γρήγορες ενέργειες",
"search_popout.recent": "Πρόσφατες αναζητήσεις",
"search_popout.specific_date": "συγκεκριμένη ημερομηνία",
"search_popout.user": "χρήστης",
"search_results.accounts": "Προφίλ",
"search_results.all": "Όλα",
@ -555,8 +709,11 @@
"server_banner.about_active_users": "Άτομα που χρησιμοποιούν αυτόν τον διακομιστή κατά τις τελευταίες 30 ημέρες (Μηνιαία Ενεργοί Χρήστες)",
"server_banner.active_users": "ενεργοί χρήστες",
"server_banner.administered_by": "Διαχειριστής:",
"server_banner.is_one_of_many": "Το {domain} είναι ένας από τους πολλούς ανεξάρτητους διακομιστές Mastodon που μπορείς να χρησιμοποιήσεις για να συμμετάσχεις στο fediverse.",
"server_banner.server_stats": "Στατιστικά διακομιστή:",
"sign_in_banner.create_account": "Δημιουργία λογαριασμού",
"sign_in_banner.follow_anyone": "Ακολούθησε οποιονδήποτε κατά μήκος του fediverse και δες τα όλα με χρονολογική σειρά. Δεν υπάρχουν αλγόριθμοι, διαφημίσεις ή clickbait ούτε για δείγμα.",
"sign_in_banner.mastodon_is": "Το Mastodon είναι ο καλύτερος τρόπος για να συμβαδίσεις με τα γεγονότα.",
"sign_in_banner.sign_in": "Σύνδεση",
"sign_in_banner.sso_redirect": "Συνδεθείτε ή Εγγραφείτε",
"status.admin_account": "Άνοιγμα διεπαφής συντονισμού για τον/την @{name}",
@ -572,15 +729,19 @@
"status.direct": "Ιδιωτική επισήμανση @{name}",
"status.direct_indicator": "Ιδιωτική επισήμανση",
"status.edit": "Επεξεργασία",
"status.edited": "Τελευταία επεξεργασία {date}",
"status.edited_x_times": "Επεξεργάστηκε {count, plural, one {{count} φορά} other {{count} φορές}}",
"status.embed": "Ενσωμάτωσε",
"status.favourite": "Αγαπημένα",
"status.favourites": "{count, plural, one {# αγαπημένο} other {# αγαπημένα}}",
"status.filter": "Φιλτράρισμα αυτής της ανάρτησης",
"status.filtered": "Φιλτραρισμένα",
"status.hide": "Απόκρυψη ανάρτησης",
"status.history.created": "{name} δημιούργησε στις {date}",
"status.history.edited": "{name} επεξεργάστηκε στις {date}",
"status.load_more": "Φόρτωσε περισσότερα",
"status.media.open": "Κάνε κλικ για άνοιγμα",
"status.media.show": "Κάνε κλικ για εμφάνιση",
"status.media_hidden": "Κρυμμένο πολυμέσο",
"status.mention": "Επισήμανε @{name}",
"status.more": "Περισσότερα",
@ -593,6 +754,7 @@
"status.reblog": "Ενίσχυση",
"status.reblog_private": "Ενίσχυση με αρχική ορατότητα",
"status.reblogged_by": "{name} προώθησε",
"status.reblogs": "{count, plural, one {# ενίσχυση} other {# ενισχύσεις}}",
"status.reblogs.empty": "Κανείς δεν ενίσχυσε αυτή την ανάρτηση ακόμα. Μόλις το κάνει κάποιος, θα εμφανιστεί εδώ.",
"status.redraft": "Σβήσε & ξαναγράψε",
"status.remove_bookmark": "Αφαίρεση σελιδοδείκτη",
@ -611,6 +773,7 @@
"status.title.with_attachments": "{user} δημοσίευσε {attachmentCount, plural, one {ένα συνημμένο} other {{attachmentCount} συνημμένα}}",
"status.translate": "Μετάφραση",
"status.translated_from_with": "Μεταφράστηκε από {lang} χρησιμοποιώντας {provider}",
"status.uncached_media_warning": "Μη διαθέσιμη προεπισκόπηση",
"status.unmute_conversation": "Αναίρεση σίγασης συνομιλίας",
"status.unpin": "Ξεκαρφίτσωσε από το προφίλ",
"subscribed_languages.lead": "Μόνο αναρτήσεις σε επιλεγμένες γλώσσες θα εμφανίζονται στην αρχική σου και θα παραθέτονται χρονοδιαγράμματα μετά την αλλαγή. Επέλεξε καμία για να λαμβάνεις αναρτήσεις σε όλες τις γλώσσες.",

View file

@ -485,7 +485,6 @@
"notification.moderation_warning.action_silence": "Your account has been limited.",
"notification.moderation_warning.action_suspend": "Your account has been suspended.",
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your status",
"notification.relationships_severance_event": "Lost connections with {name}",
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",

View file

@ -552,6 +552,8 @@
"mute_modal.title": "Mute user?",
"mute_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.",
"name_and_others": "{name} and {count, plural, one {# other} other {# others}}",
"name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a>",
"navigation_bar.about": "About",
"navigation_bar.advanced_interface": "Open in advanced web interface",
"navigation_bar.antennas": "Antenna",
@ -585,6 +587,10 @@
"navigation_bar.security": "Security",
"not_signed_in_indicator.not_signed_in": "You need to login to access this resource.",
"notification.admin.report": "{name} reported {target}",
"notification.admin.report_account": "{name} reported {count, plural, one {one post} other {# posts}} from {target} for {category}",
"notification.admin.report_account_other": "{name} reported {count, plural, one {one post} other {# posts}} from {target}",
"notification.admin.report_statuses": "{name} reported {target} for {category}",
"notification.admin.report_statuses_other": "{name} reported {target}",
"notification.admin.sign_up": "{name} signed up",
"notification.emoji_reaction": "{name} reacted your post with emoji",
"notification.favourite": "{name} favorited your post",
@ -603,7 +609,8 @@
"notification.moderation_warning.action_silence": "Your account has been limited.",
"notification.moderation_warning.action_suspend": "Your account has been suspended.",
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended",
"notification.poll": "A poll you voted in has ended",
"notification.private_mention": "{name} privately mentioned you",
"notification.reblog": "{name} boosted your post",
"notification.relationships_severance_event": "Lost connections with {name}",
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
@ -622,7 +629,9 @@
"notifications.column_settings.admin.report": "New reports:",
"notifications.column_settings.admin.sign_up": "New sign-ups:",
"notifications.column_settings.alert": "Desktop notifications",
"notifications.column_settings.emoji_reaction": "Stamps:",
"notifications.column_settings.beta.category": "Experimental features",
"notifications.column_settings.beta.grouping": "Group notifications",
"notifications.column_settings.emoji_reaction": "Emoji reactions:",
"notifications.column_settings.favourite": "Favorites:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
@ -807,9 +816,13 @@
"report.unfollow_explanation": "You are following this account. To not see their posts in your home feed anymore, unfollow them.",
"report_notification.attached_statuses": "{count, plural, one {{count} post} other {{count} posts}} attached",
"report_notification.categories.legal": "Legal",
"report_notification.categories.legal_sentence": "illegal content",
"report_notification.categories.other": "Other",
"report_notification.categories.other_sentence": "other",
"report_notification.categories.spam": "Spam",
"report_notification.categories.spam_sentence": "spam",
"report_notification.categories.violation": "Rule violation",
"report_notification.categories.violation_sentence": "rule violation",
"report_notification.open": "Open report",
"search.no_recent_searches": "No recent searches",
"search.placeholder": "Search",

View file

@ -405,7 +405,6 @@
"notification.follow_request": "{name} petis sekvi vin",
"notification.mention": "{name} menciis vin",
"notification.own_poll": "Via enketo finiĝis",
"notification.poll": "Partoprenita balotenketo finiĝis",
"notification.reblog": "{name} diskonigis vian afiŝon",
"notification.status": "{name} ĵus afiŝis",
"notification.update": "{name} redaktis afiŝon",

View file

@ -443,6 +443,8 @@
"mute_modal.title": "¿Silenciar usuario?",
"mute_modal.you_wont_see_mentions": "No verás mensajes que los mencionen.",
"mute_modal.you_wont_see_posts": "Todavía pueden ver tus mensajes, pero vos no verás los suyos.",
"name_and_others": "{name} y {count, plural, one {# cuenta más} other {# cuentas más}}",
"name_and_others_with_link": "{name} y <a>{count, plural, one {# cuenta más} other {# cuentas más}}</a>",
"navigation_bar.about": "Información",
"navigation_bar.advanced_interface": "Abrir en interface web avanzada",
"navigation_bar.blocks": "Usuarios bloqueados",
@ -470,6 +472,10 @@
"navigation_bar.security": "Seguridad",
"not_signed_in_indicator.not_signed_in": "Necesitás iniciar sesión para acceder a este recurso.",
"notification.admin.report": "{name} denunció a {target}",
"notification.admin.report_account": "{name} denunció {count, plural, one {un mensaje} other {# mensajes}} de {target} por {category}",
"notification.admin.report_account_other": "{name} denunció {count, plural, one {un mensaje} other {# mensajes}} de {target}",
"notification.admin.report_statuses": "{name} denunció a {target} por {category}",
"notification.admin.report_statuses_other": "{name} denunció a {target}",
"notification.admin.sign_up": "Se registró {name}",
"notification.favourite": "{name} marcó tu mensaje como favorito",
"notification.follow": "{name} te empezó a seguir",
@ -486,6 +492,7 @@
"notification.moderation_warning.action_suspend": "Tu cuenta fue suspendida.",
"notification.own_poll": "Tu encuesta finalizó",
"notification.poll": "Finalizó una encuesta en la que votaste",
"notification.private_mention": "{name} te mencionó en privado",
"notification.reblog": "{name} adhirió a tu mensaje",
"notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} suspendió a {target}, lo que significa que ya no podés recibir actualizaciones de esa cuenta o interactuar con la misma.",
@ -503,6 +510,8 @@
"notifications.column_settings.admin.report": "Nuevas denuncias:",
"notifications.column_settings.admin.sign_up": "Nuevos registros:",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.beta.category": "Funciones experimentales",
"notifications.column_settings.beta.grouping": "Agrupar notificaciones",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
@ -666,9 +675,13 @@
"report.unfollow_explanation": "Estás siguiendo a esta cuenta. Para no ver sus mensajes en tu línea temporal principal, dejá de seguirla.",
"report_notification.attached_statuses": "{count, plural, one {{count} mensaje adjunto} other {{count} mensajes adjuntos}}",
"report_notification.categories.legal": "Legal",
"report_notification.categories.legal_sentence": "contenido ilegal",
"report_notification.categories.other": "Otros",
"report_notification.categories.other_sentence": "[otras categorías]",
"report_notification.categories.spam": "Spam",
"report_notification.categories.spam_sentence": "spam",
"report_notification.categories.violation": "Violación de regla",
"report_notification.categories.violation_sentence": "violación de regla",
"report_notification.open": "Abrir denuncia",
"search.no_recent_searches": "Sin búsquedas recientes",
"search.placeholder": "Buscar",

View file

@ -443,6 +443,8 @@
"mute_modal.title": "¿Silenciar usuario?",
"mute_modal.you_wont_see_mentions": "No verás publicaciones que los mencionen.",
"mute_modal.you_wont_see_posts": "Todavía pueden ver tus publicaciones, pero tú no verás las de ellos.",
"name_and_others": "{name} y {count, plural, one {# más} other {# más}}",
"name_and_others_with_link": "{name} y <a>{count, plural, one {# más} other {# más}}</a>",
"navigation_bar.about": "Acerca de",
"navigation_bar.advanced_interface": "Abrir en interfaz web avanzada",
"navigation_bar.blocks": "Usuarios bloqueados",
@ -470,6 +472,10 @@
"navigation_bar.security": "Seguridad",
"not_signed_in_indicator.not_signed_in": "Necesitas iniciar sesión para acceder a este recurso.",
"notification.admin.report": "{name} denunció a {target}",
"notification.admin.report_account": "{name} informó de {count, plural, one {una publicación} other {# publicaciones}} de {target} por {category}",
"notification.admin.report_account_other": "{name} informó de {count, plural, one {una publicación} other {# publicaciones}} de {target}",
"notification.admin.report_statuses": "{name} informó de {target} por {category}",
"notification.admin.report_statuses_other": "{name} informó de {target}",
"notification.admin.sign_up": "{name} se unio",
"notification.favourite": "{name} marcó como favorita tu publicación",
"notification.follow": "{name} te empezó a seguir",
@ -485,7 +491,8 @@
"notification.moderation_warning.action_silence": "Tu cuenta ha sido limitada.",
"notification.moderation_warning.action_suspend": "Tu cuenta ha sido suspendida.",
"notification.own_poll": "Tu encuesta ha terminado",
"notification.poll": "Una encuesta en la que has votado ha terminado",
"notification.poll": "Una encuesta ha terminado",
"notification.private_mention": "{name} te mencionó en privado",
"notification.reblog": "{name} ha retooteado tu estado",
"notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspendido {target}, lo que significa que ya no puedes recibir actualizaciones de sus cuentas o interactuar con ellas.",
@ -503,6 +510,8 @@
"notifications.column_settings.admin.report": "Nuevas denuncias:",
"notifications.column_settings.admin.sign_up": "Registros nuevos:",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.beta.category": "Características experimentales",
"notifications.column_settings.beta.grouping": "Agrupar notificaciones",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
@ -666,9 +675,13 @@
"report.unfollow_explanation": "Estás siguiendo esta cuenta. Para no ver sus publicaciones en tu inicio, deja de seguirla.",
"report_notification.attached_statuses": "{count, plural, one {{count} publicación} other {{count} publicaciones}} adjunta(s)",
"report_notification.categories.legal": "Legal",
"report_notification.categories.legal_sentence": "contenido ilegal",
"report_notification.categories.other": "Otro",
"report_notification.categories.other_sentence": "otra",
"report_notification.categories.spam": "Spam",
"report_notification.categories.spam_sentence": "spam",
"report_notification.categories.violation": "Infracción de regla",
"report_notification.categories.violation_sentence": "infracción de regla",
"report_notification.open": "Abrir denuncia",
"search.no_recent_searches": "Sin búsquedas recientes",
"search.placeholder": "Buscar",

View file

@ -443,6 +443,8 @@
"mute_modal.title": "¿Silenciar usuario?",
"mute_modal.you_wont_see_mentions": "No verás mensajes que los mencionen.",
"mute_modal.you_wont_see_posts": "Todavía pueden ver tus publicaciones, pero tú no verás las suyas.",
"name_and_others": "{name} y {count, plural, one {# más} other {# más}}",
"name_and_others_with_link": "{name} y <a>{count, plural, one {# más} other {# más}}</a>",
"navigation_bar.about": "Acerca de",
"navigation_bar.advanced_interface": "Abrir en la interfaz web avanzada",
"navigation_bar.blocks": "Usuarios bloqueados",
@ -470,6 +472,10 @@
"navigation_bar.security": "Seguridad",
"not_signed_in_indicator.not_signed_in": "Necesitas iniciar sesión para acceder a este recurso.",
"notification.admin.report": "{name} informó {target}",
"notification.admin.report_account": "{name} informó de {count, plural, one {una publicación} other {# publicaciones}} de {target} por {category}",
"notification.admin.report_account_other": "{name} informó de {count, plural, one {una publicación} other {# publicaciones}} de {target}",
"notification.admin.report_statuses": "{name} informó de {target} por {category}",
"notification.admin.report_statuses_other": "{name} informó de {target}",
"notification.admin.sign_up": "{name} se registró",
"notification.favourite": "{name} marcó como favorita tu publicación",
"notification.follow": "{name} te empezó a seguir",
@ -485,7 +491,8 @@
"notification.moderation_warning.action_silence": "Tu cuenta ha sido limitada.",
"notification.moderation_warning.action_suspend": "Tu cuenta ha sido suspendida.",
"notification.own_poll": "Tu encuesta ha terminado",
"notification.poll": "Una encuesta en la que has votado ha terminado",
"notification.poll": "Una encuesta ha terminado",
"notification.private_mention": "{name} te mencionó en privado",
"notification.reblog": "{name} ha impulsado tu publicación",
"notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspendido {target}, lo que significa que ya no puedes recibir actualizaciones de sus cuentas o interactuar con ellas.",
@ -503,6 +510,8 @@
"notifications.column_settings.admin.report": "Nuevos informes:",
"notifications.column_settings.admin.sign_up": "Nuevos registros:",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.beta.category": "Características experimentales",
"notifications.column_settings.beta.grouping": "Agrupar notificaciones",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
@ -666,9 +675,13 @@
"report.unfollow_explanation": "Estás siguiendo esta cuenta. Para no ver sus publicaciones en tu muro de inicio, deja de seguirla.",
"report_notification.attached_statuses": "{count, plural, one {{count} publicación} other {{count} publicaciones}} adjunta(s)",
"report_notification.categories.legal": "Legal",
"report_notification.categories.legal_sentence": "contenido ilegal",
"report_notification.categories.other": "Otros",
"report_notification.categories.other_sentence": "otra",
"report_notification.categories.spam": "Spam",
"report_notification.categories.spam_sentence": "spam",
"report_notification.categories.violation": "Infracción de regla",
"report_notification.categories.violation_sentence": "infracción de regla",
"report_notification.open": "Abrir informe",
"search.no_recent_searches": "No hay búsquedas recientes",
"search.placeholder": "Buscar",

View file

@ -482,7 +482,6 @@
"notification.moderation_warning.action_silence": "Su kontole pandi piirang.",
"notification.moderation_warning.action_suspend": "Su konto on peatatud.",
"notification.own_poll": "Su küsitlus on lõppenud",
"notification.poll": "Küsitlus, milles osalesid, on lõppenud",
"notification.reblog": "{name} jagas edasi postitust",
"notification.relationships_severance_event": "Kadunud ühendus kasutajaga {name}",
"notification.relationships_severance_event.account_suspension": "{from} admin on kustutanud {target}, mis tähendab, et sa ei saa enam neilt uuendusi või suhelda nendega.",

View file

@ -480,7 +480,6 @@
"notification.moderation_warning.action_silence": "Kontua murriztu egin da.",
"notification.moderation_warning.action_suspend": "Kontua itxi da.",
"notification.own_poll": "Zure inkesta amaitu da",
"notification.poll": "Zuk erantzun duzun inkesta bat bukatu da",
"notification.reblog": "{name}(e)k bultzada eman dio zure bidalketari",
"notification.relationships_severance_event": "{name} erabiltzailearekin galdutako konexioak",
"notification.relationships_severance_event.account_suspension": "{from} zerbitzariko administratzaile batek {target} bertan behera utzi du, hau da, ezin izango dituzu jaso hango eguneratzerik edo hangoekin elkarreragin.",

View file

@ -424,7 +424,6 @@
"notification.follow_request": "{name} درخواست پی‌گیریتان را داد",
"notification.mention": "{name} به شما اشاره کرد",
"notification.own_poll": "نظرسنجیتان پایان یافت",
"notification.poll": "نظرسنجی‌ای که در آن رأی دادید به پایان رسیده است",
"notification.reblog": "{name} فرسته‌تان را تقویت کرد",
"notification.status": "{name} چیزی فرستاد",
"notification.update": "{name} فرسته‌ای را ویرایش کرد",

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