Merge pull request #2846 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to e0648a916a
This commit is contained in:
Claire 2024-09-16 23:31:03 +02:00 committed by GitHub
commit c8ef702ba9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 2093 additions and 1161 deletions

View file

@ -7,8 +7,13 @@ RSpec/Focus: # Require full spec run on CI
Exclude: []
Rails/Output: # Remove any `puts` debugging
inherit_mode:
merge:
- Include
Enabled: true
Exclude: []
Include:
- spec/**/*.rb
Rails/FindEach: # Using `each` could impact performance, use `find_each`
Enabled: true

View file

@ -331,7 +331,7 @@ GEM
httplog (1.7.0)
rack (>= 2.0)
rainbow (>= 2.0.0)
i18n (1.14.5)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.14)
activesupport (>= 4.0.2)
@ -472,9 +472,9 @@ GEM
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth-saml (2.1.0)
omniauth (~> 2.0)
ruby-saml (~> 1.12)
omniauth-saml (2.2.1)
omniauth (~> 2.1)
ruby-saml (~> 1.17)
omniauth_openid_connect (0.6.1)
omniauth (>= 1.9, < 3)
openid_connect (~> 1.1)
@ -764,7 +764,7 @@ GEM
rubocop-rspec (~> 3, >= 3.0.1)
ruby-prof (1.7.0)
ruby-progressbar (1.13.0)
ruby-saml (1.16.0)
ruby-saml (1.17.0)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.2.2)

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::V2Alpha::Notifications::AccountsController < Api::BaseController
class Api::V2::Notifications::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }
before_action :require_user!
before_action :set_notifications!
@ -33,11 +33,11 @@ class Api::V2Alpha::Notifications::AccountsController < Api::BaseController
end
def next_path
api_v2_alpha_notification_accounts_url pagination_params(max_id: pagination_max_id) if records_continue?
api_v2_notification_accounts_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v2_alpha_notification_accounts_url pagination_params(min_id: pagination_since_id) unless @paginated_notifications.empty?
api_v2_notification_accounts_url pagination_params(min_id: pagination_since_id) unless @paginated_notifications.empty?
end
def pagination_collection

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::V2Alpha::NotificationsController < Api::BaseController
class Api::V2::NotificationsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss]
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss]
before_action :require_user!
@ -21,7 +21,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
ActiveRecord::Associations::Preloader.new(records: @presenter.accounts, associations: [:account_stat, { user: :role }]).call
end
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#index rendering') do |span|
MastodonOTELTracer.in_span('Api::V2::NotificationsController#index rendering') do |span|
statuses = @grouped_notifications.filter_map { |group| group.target_status&.id }
span.add_attributes(
@ -64,7 +64,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
private
def load_notifications
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_notifications') do
MastodonOTELTracer.in_span('Api::V2::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, :grouped_types).permit(:max_id, :since_id, :min_id, grouped_types: [])
@ -79,7 +79,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
def load_grouped_notifications
return [] if @notifications.empty?
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
end
end
@ -101,11 +101,11 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
end
def next_path
api_v2_alpha_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty?
api_v2_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty?
end
def prev_path
api_v2_alpha_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
api_v2_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
end
def pagination_collection

View file

@ -2,7 +2,7 @@ import { createAction } from '@reduxjs/toolkit';
import {
apiClearNotifications,
apiFetchNotifications,
apiFetchNotificationGroups,
} from 'flavours/glitch/api/notifications';
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
import type {
@ -71,7 +71,7 @@ function dispatchAssociatedRecords(
export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) =>
apiFetchNotifications({ exclude_types: getExcludedTypes(getState()) }),
apiFetchNotificationGroups({ exclude_types: getExcludedTypes(getState()) }),
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
@ -92,7 +92,7 @@ export const fetchNotifications = createDataLoadingThunk(
export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }, { getState }) =>
apiFetchNotifications({
apiFetchNotificationGroups({
max_id: params.gap.maxId,
exclude_types: getExcludedTypes(getState()),
}),
@ -108,7 +108,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => {
return apiFetchNotifications({
return apiFetchNotificationGroups({
max_id: undefined,
exclude_types: getExcludedTypes(getState()),
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones

View file

@ -0,0 +1,234 @@
import {
apiFetchNotificationRequest,
apiFetchNotificationRequests,
apiFetchNotifications,
apiAcceptNotificationRequest,
apiDismissNotificationRequest,
apiAcceptNotificationRequests,
apiDismissNotificationRequests,
} from 'flavours/glitch/api/notifications';
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
} from 'flavours/glitch/api_types/notifications';
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
import type { AppDispatch, RootState } from 'flavours/glitch/store';
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { decreasePendingNotificationsCount } from './notification_policies';
// TODO: refactor with notification_groups
function dispatchAssociatedRecords(
dispatch: AppDispatch,
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];
notifications.forEach((notification) => {
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) {
fetchedStatuses.push(notification.status);
}
});
if (fetchedAccounts.length > 0)
dispatch(importFetchedAccounts(fetchedAccounts));
if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
}
export const fetchNotificationRequests = createDataLoadingThunk(
'notificationRequests/fetch',
async (_params, { getState }) => {
let sinceId = undefined;
if (getState().notificationRequests.items.length > 0) {
sinceId = getState().notificationRequests.items[0]?.id;
}
return apiFetchNotificationRequests({
since_id: sinceId,
});
},
({ requests, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatch(importFetchedAccounts(requests.map((request) => request.account)));
return { requests, next: next?.uri };
},
{
condition: (_params, { getState }) =>
!getState().notificationRequests.isLoading,
},
);
export const fetchNotificationRequest = createDataLoadingThunk(
'notificationRequest/fetch',
async ({ id }: { id: string }) => apiFetchNotificationRequest(id),
{
condition: ({ id }, { getState }) =>
!(
getState().notificationRequests.current.item?.id === id ||
getState().notificationRequests.current.isLoading
),
},
);
export const expandNotificationRequests = createDataLoadingThunk(
'notificationRequests/expand',
async (_, { getState }) => {
const nextUrl = getState().notificationRequests.next;
if (!nextUrl) throw new Error('missing URL');
return apiFetchNotificationRequests(undefined, nextUrl);
},
({ requests, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatch(importFetchedAccounts(requests.map((request) => request.account)));
return { requests, next: next?.uri };
},
{
condition: (_, { getState }) =>
!!getState().notificationRequests.next &&
!getState().notificationRequests.isLoading,
},
);
export const fetchNotificationsForRequest = createDataLoadingThunk(
'notificationRequest/fetchNotifications',
async ({ accountId }: { accountId: string }, { getState }) => {
const sinceId =
// @ts-expect-error current.notifications.items is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
getState().notificationRequests.current.notifications.items[0]?.get(
'id',
) as string | undefined;
return apiFetchNotifications({
since_id: sinceId,
account_id: accountId,
});
},
({ notifications, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatchAssociatedRecords(dispatch, notifications);
return { notifications, next: next?.uri };
},
{
condition: ({ accountId }, { getState }) => {
const current = getState().notificationRequests.current;
return !(
current.item?.account_id === accountId &&
current.notifications.isLoading
);
},
},
);
export const expandNotificationsForRequest = createDataLoadingThunk(
'notificationRequest/expandNotifications',
async (_, { getState }) => {
const nextUrl = getState().notificationRequests.current.notifications.next;
if (!nextUrl) throw new Error('missing URL');
return apiFetchNotifications(undefined, nextUrl);
},
({ notifications, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatchAssociatedRecords(dispatch, notifications);
return { notifications, next: next?.uri };
},
{
condition: ({ accountId }: { accountId: string }, { getState }) => {
const url = getState().notificationRequests.current.notifications.next;
return (
!!url &&
!getState().notificationRequests.current.notifications.isLoading &&
getState().notificationRequests.current.item?.account_id === accountId
);
},
},
);
const selectNotificationCountForRequest = (state: RootState, id: string) => {
const requests = state.notificationRequests.items;
const thisRequest = requests.find((request) => request.id === id);
return thisRequest ? thisRequest.notifications_count : 0;
};
export const acceptNotificationRequest = createDataLoadingThunk(
'notificationRequest/accept',
({ id }: { id: string }) => apiAcceptNotificationRequest(id),
(_data, { dispatch, getState, discardLoadData, actionArg: { id } }) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(decreasePendingNotificationsCount(count));
// The payload is not used in any functions
return discardLoadData;
},
);
export const dismissNotificationRequest = createDataLoadingThunk(
'notificationRequest/dismiss',
({ id }: { id: string }) => apiDismissNotificationRequest(id),
(_data, { dispatch, getState, discardLoadData, actionArg: { id } }) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(decreasePendingNotificationsCount(count));
// The payload is not used in any functions
return discardLoadData;
},
);
export const acceptNotificationRequests = createDataLoadingThunk(
'notificationRequests/acceptBulk',
({ ids }: { ids: string[] }) => apiAcceptNotificationRequests(ids),
(_data, { dispatch, getState, discardLoadData, actionArg: { ids } }) => {
const count = ids.reduce(
(count, id) => count + selectNotificationCountForRequest(getState(), id),
0,
);
dispatch(decreasePendingNotificationsCount(count));
// The payload is not used in any functions
return discardLoadData;
},
);
export const dismissNotificationRequests = createDataLoadingThunk(
'notificationRequests/dismissBulk',
({ ids }: { ids: string[] }) => apiDismissNotificationRequests(ids),
(_data, { dispatch, getState, discardLoadData, actionArg: { ids } }) => {
const count = ids.reduce(
(count, id) => count + selectNotificationCountForRequest(getState(), id),
0,
);
dispatch(decreasePendingNotificationsCount(count));
// The payload is not used in any functions
return discardLoadData;
},
);

View file

@ -18,7 +18,6 @@ import {
importFetchedStatuses,
} from './importer';
import { submitMarkers } from './markers';
import { decreasePendingNotificationsCount } from './notification_policies';
import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings';
@ -57,26 +56,6 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST';
export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS';
export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL';
export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST';
export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS';
export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL';
export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST';
export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL';
export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST';
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';
@ -85,14 +64,6 @@ export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISM
export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL';
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
});
@ -105,12 +76,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
}
};
const selectNotificationCountForRequest = (state, id) => {
const requests = state.getIn(['notificationRequests', 'items']);
const thisRequest = requests.find(request => request.get('id') === id);
return thisRequest ? thisRequest.get('notifications_count') : 0;
};
export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING,
});
@ -431,296 +396,3 @@ export function setBrowserPermission (value) {
value,
};
}
export const fetchNotificationRequests = () => (dispatch, getState) => {
const params = {};
if (getState().getIn(['notificationRequests', 'isLoading'])) {
return;
}
if (getState().getIn(['notificationRequests', 'items'])?.size > 0) {
params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']);
}
dispatch(fetchNotificationRequestsRequest());
api().get('/api/v1/notifications/requests', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchNotificationRequestsFail(err));
});
};
export const fetchNotificationRequestsRequest = () => ({
type: NOTIFICATION_REQUESTS_FETCH_REQUEST,
});
export const fetchNotificationRequestsSuccess = (requests, next) => ({
type: NOTIFICATION_REQUESTS_FETCH_SUCCESS,
requests,
next,
});
export const fetchNotificationRequestsFail = error => ({
type: NOTIFICATION_REQUESTS_FETCH_FAIL,
error,
});
export const expandNotificationRequests = () => (dispatch, getState) => {
const url = getState().getIn(['notificationRequests', 'next']);
if (!url || getState().getIn(['notificationRequests', 'isLoading'])) {
return;
}
dispatch(expandNotificationRequestsRequest());
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(expandNotificationRequestsSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(expandNotificationRequestsFail(err));
});
};
export const expandNotificationRequestsRequest = () => ({
type: NOTIFICATION_REQUESTS_EXPAND_REQUEST,
});
export const expandNotificationRequestsSuccess = (requests, next) => ({
type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
requests,
next,
});
export const expandNotificationRequestsFail = error => ({
type: NOTIFICATION_REQUESTS_EXPAND_FAIL,
error,
});
export const fetchNotificationRequest = id => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
if (current.getIn(['item', 'id']) === id || current.get('isLoading')) {
return;
}
dispatch(fetchNotificationRequestRequest(id));
api().get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
dispatch(fetchNotificationRequestSuccess(data));
}).catch(err => {
dispatch(fetchNotificationRequestFail(id, err));
});
};
export const fetchNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_FETCH_REQUEST,
id,
});
export const fetchNotificationRequestSuccess = request => ({
type: NOTIFICATION_REQUEST_FETCH_SUCCESS,
request,
});
export const fetchNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_FETCH_FAIL,
id,
error,
});
export const acceptNotificationRequest = (id) => (dispatch, getState) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(acceptNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
dispatch(acceptNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(id, err));
});
};
export const acceptNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_ACCEPT_REQUEST,
id,
});
export const acceptNotificationRequestSuccess = id => ({
type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS,
id,
});
export const acceptNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_ACCEPT_FAIL,
id,
error,
});
export const dismissNotificationRequest = (id) => (dispatch, getState) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(dismissNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
dispatch(dismissNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(id, err));
});
};
export const dismissNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_DISMISS_REQUEST,
id,
});
export const dismissNotificationRequestSuccess = id => ({
type: NOTIFICATION_REQUEST_DISMISS_SUCCESS,
id,
});
export const dismissNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_DISMISS_FAIL,
id,
error,
});
export const acceptNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => {
dispatch(acceptNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(ids, err));
});
};
export const acceptNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
ids,
});
export const acceptNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS,
ids,
});
export const acceptNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_ACCEPT_FAIL,
ids,
error,
});
export const dismissNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => {
dispatch(dismissNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(ids, err));
});
};
export const dismissNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_REQUEST,
ids,
});
export const dismissNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS,
ids,
});
export const dismissNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_DISMISS_FAIL,
ids,
error,
});
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
const params = { account_id: accountId };
if (current.getIn(['item', 'account']) === accountId) {
if (current.getIn(['notifications', 'isLoading'])) {
return;
}
if (current.getIn(['notifications', 'items'])?.size > 0) {
params.since_id = current.getIn(['notifications', 'items', 0, 'id']);
}
}
dispatch(fetchNotificationsForRequestRequest());
api().get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(fetchNotificationsForRequestFail(err));
});
};
export const fetchNotificationsForRequestRequest = () => ({
type: NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
});
export const fetchNotificationsForRequestSuccess = (notifications, next) => ({
type: NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
notifications,
next,
});
export const fetchNotificationsForRequestFail = (error) => ({
type: NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
error,
});
export const expandNotificationsForRequest = () => (dispatch, getState) => {
const url = getState().getIn(['notificationRequests', 'current', 'notifications', 'next']);
if (!url || getState().getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])) {
return;
}
dispatch(expandNotificationsForRequestRequest());
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(expandNotificationsForRequestFail(err));
});
};
export const expandNotificationsForRequestRequest = () => ({
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
});
export const expandNotificationsForRequestSuccess = (notifications, next) => ({
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
notifications,
next,
});
export const expandNotificationsForRequestFail = (error) => ({
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
error,
});

View file

@ -1,14 +1,43 @@
import api, { apiRequest, getLinks } from 'flavours/glitch/api';
import type { ApiNotificationGroupsResultJSON } from 'flavours/glitch/api_types/notifications';
import api, {
apiRequest,
getLinks,
apiRequestGet,
apiRequestPost,
} from 'flavours/glitch/api';
import type {
ApiNotificationGroupsResultJSON,
ApiNotificationRequestJSON,
ApiNotificationJSON,
} from 'flavours/glitch/api_types/notifications';
export const apiFetchNotifications = async (params?: {
export const apiFetchNotifications = async (
params?: {
account_id?: string;
since_id?: string;
},
url?: string,
) => {
const response = await api().request<ApiNotificationJSON[]>({
method: 'GET',
url: url ?? '/api/v1/notifications',
params,
});
return {
notifications: response.data,
links: getLinks(response),
};
};
export const apiFetchNotificationGroups = async (params?: {
url?: string;
exclude_types?: string[];
max_id?: string;
since_id?: string;
}) => {
const response = await api().request<ApiNotificationGroupsResultJSON>({
method: 'GET',
url: '/api/v2_alpha/notifications',
url: '/api/v2/notifications',
params,
});
@ -24,3 +53,43 @@ export const apiFetchNotifications = async (params?: {
export const apiClearNotifications = () =>
apiRequest<undefined>('POST', 'v1/notifications/clear');
export const apiFetchNotificationRequests = async (
params?: {
since_id?: string;
},
url?: string,
) => {
const response = await api().request<ApiNotificationRequestJSON[]>({
method: 'GET',
url: url ?? '/api/v1/notifications/requests',
params,
});
return {
requests: response.data,
links: getLinks(response),
};
};
export const apiFetchNotificationRequest = async (id: string) => {
return apiRequestGet<ApiNotificationRequestJSON>(
`v1/notifications/requests/${id}`,
);
};
export const apiAcceptNotificationRequest = async (id: string) => {
return apiRequestPost(`v1/notifications/requests/${id}/accept`);
};
export const apiDismissNotificationRequest = async (id: string) => {
return apiRequestPost(`v1/notifications/requests/${id}/dismiss`);
};
export const apiAcceptNotificationRequests = async (id: string[]) => {
return apiRequestPost('v1/notifications/requests/accept', { id });
};
export const apiDismissNotificationRequests = async (id: string[]) => {
return apiRequestPost('v1/notifications/dismiss/dismiss', { id });
};

View file

@ -149,3 +149,12 @@ export interface ApiNotificationGroupsResultJSON {
statuses: ApiStatusJSON[];
notification_groups: ApiNotificationGroupJSON[];
}
export interface ApiNotificationRequestJSON {
id: string;
created_at: string;
updated_at: string;
notifications_count: string;
account: ApiAccountJSON;
last_status?: ApiStatusJSON;
}

View file

@ -12,7 +12,7 @@ import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notifications';
import { acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notification_requests';
import { initReport } from 'flavours/glitch/actions/reports';
import { Avatar } from 'flavours/glitch/components/avatar';
import { CheckBox } from 'flavours/glitch/components/check_box';
@ -40,11 +40,11 @@ export const NotificationRequest = ({ id, accountId, notificationsCount, checked
const { push: historyPush } = useHistory();
const handleDismiss = useCallback(() => {
dispatch(dismissNotificationRequest(id));
dispatch(dismissNotificationRequest({ id }));
}, [dispatch, id]);
const handleAccept = useCallback(() => {
dispatch(acceptNotificationRequest(id));
dispatch(acceptNotificationRequest({ id }));
}, [dispatch, id]);
const handleMute = useCallback(() => {

View file

@ -10,7 +10,13 @@ import { useSelector, useDispatch } from 'react-redux';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
import { fetchNotificationRequest, fetchNotificationsForRequest, expandNotificationsForRequest, acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notifications';
import {
fetchNotificationRequest,
fetchNotificationsForRequest,
expandNotificationsForRequest,
acceptNotificationRequest,
dismissNotificationRequest,
} from 'flavours/glitch/actions/notification_requests';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
import { IconButton } from 'flavours/glitch/components/icon_button';
@ -44,28 +50,28 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
const columnRef = useRef();
const intl = useIntl();
const dispatch = useDispatch();
const notificationRequest = useSelector(state => state.getIn(['notificationRequests', 'current', 'item', 'id']) === id ? state.getIn(['notificationRequests', 'current', 'item']) : null);
const accountId = notificationRequest?.get('account');
const notificationRequest = useSelector(state => state.notificationRequests.current.item?.id === id ? state.notificationRequests.current.item : null);
const accountId = notificationRequest?.account_id;
const account = useSelector(state => state.getIn(['accounts', accountId]));
const notifications = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'items']));
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'isLoading']));
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'current', 'notifications', 'next']));
const removed = useSelector(state => state.getIn(['notificationRequests', 'current', 'removed']));
const notifications = useSelector(state => state.notificationRequests.current.notifications.items);
const isLoading = useSelector(state => state.notificationRequests.current.notifications.isLoading);
const hasMore = useSelector(state => !!state.notificationRequests.current.notifications.next);
const removed = useSelector(state => state.notificationRequests.current.removed);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, [columnRef]);
const handleLoadMore = useCallback(() => {
dispatch(expandNotificationsForRequest());
}, [dispatch]);
dispatch(expandNotificationsForRequest({ accountId }));
}, [dispatch, accountId]);
const handleDismiss = useCallback(() => {
dispatch(dismissNotificationRequest(id));
dispatch(dismissNotificationRequest({ id }));
}, [dispatch, id]);
const handleAccept = useCallback(() => {
dispatch(acceptNotificationRequest(id));
dispatch(acceptNotificationRequest({ id }));
}, [dispatch, id]);
const handleMoveUp = useCallback(id => {
@ -79,12 +85,12 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
}, [columnRef, notifications]);
useEffect(() => {
dispatch(fetchNotificationRequest(id));
dispatch(fetchNotificationRequest({ id }));
}, [dispatch, id]);
useEffect(() => {
if (accountId) {
dispatch(fetchNotificationsForRequest(accountId));
dispatch(fetchNotificationsForRequest({ accountId }));
}
}, [dispatch, accountId]);

View file

@ -11,7 +11,12 @@ import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?rea
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
import { fetchNotificationRequests, expandNotificationRequests, acceptNotificationRequests, dismissNotificationRequests } from 'flavours/glitch/actions/notifications';
import {
fetchNotificationRequests,
expandNotificationRequests,
acceptNotificationRequests,
dismissNotificationRequests,
} from 'flavours/glitch/actions/notification_requests';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { CheckBox } from 'flavours/glitch/components/check_box';
import Column from 'flavours/glitch/components/column';
@ -84,7 +89,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
message: intl.formatMessage(messages.confirmAcceptMultipleMessage, { count: selectedItems.length }),
confirm: intl.formatMessage(messages.confirmAcceptMultipleButton, { count: selectedItems.length}),
onConfirm: () =>
dispatch(acceptNotificationRequests(selectedItems)),
dispatch(acceptNotificationRequests({ ids: selectedItems })),
},
}));
}, [dispatch, intl, selectedItems]);
@ -97,7 +102,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
message: intl.formatMessage(messages.confirmDismissMultipleMessage, { count: selectedItems.length }),
confirm: intl.formatMessage(messages.confirmDismissMultipleButton, { count: selectedItems.length}),
onConfirm: () =>
dispatch(dismissNotificationRequests(selectedItems)),
dispatch(dismissNotificationRequests({ ids: selectedItems })),
},
}));
}, [dispatch, intl, selectedItems]);
@ -161,9 +166,9 @@ export const NotificationRequests = ({ multiColumn }) => {
const columnRef = useRef();
const intl = useIntl();
const dispatch = useDispatch();
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'isLoading']));
const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next']));
const isLoading = useSelector(state => state.notificationRequests.isLoading);
const notificationRequests = useSelector(state => state.notificationRequests.items);
const hasMore = useSelector(state => !!state.notificationRequests.next);
const [selectionMode, setSelectionMode] = useState(false);
const [checkedRequestIds, setCheckedRequestIds] = useState([]);
@ -182,7 +187,7 @@ export const NotificationRequests = ({ multiColumn }) => {
else
ids.push(id);
setSelectAllChecked(ids.length === notificationRequests.size);
setSelectAllChecked(ids.length === notificationRequests.length);
return [...ids];
});
@ -193,7 +198,7 @@ export const NotificationRequests = ({ multiColumn }) => {
if(checked)
setCheckedRequestIds([]);
else
setCheckedRequestIds(notificationRequests.map(request => request.get('id')).toArray());
setCheckedRequestIds(notificationRequests.map(request => request.id));
return !checked;
});
@ -217,7 +222,7 @@ export const NotificationRequests = ({ multiColumn }) => {
multiColumn={multiColumn}
showBackButton
appendContent={
notificationRequests.size > 0 && (
notificationRequests.length > 0 && (
<SelectRow selectionMode={selectionMode} setSelectionMode={setSelectionMode} selectAllChecked={selectAllChecked} toggleSelectAll={toggleSelectAll} selectedItems={checkedRequestIds} />
)}
>
@ -236,12 +241,12 @@ export const NotificationRequests = ({ multiColumn }) => {
>
{notificationRequests.map(request => (
<NotificationRequest
key={request.get('id')}
id={request.get('id')}
accountId={request.get('account')}
notificationsCount={request.get('notifications_count')}
key={request.id}
id={request.id}
accountId={request.account_id}
notificationsCount={request.notifications_count}
showCheckbox={selectionMode}
checked={checkedRequestIds.includes(request.get('id'))}
checked={checkedRequestIds.includes(request.id)}
toggleCheck={handleCheck}
/>
))}

View file

@ -0,0 +1,19 @@
import type { ApiNotificationRequestJSON } from 'flavours/glitch/api_types/notifications';
export interface NotificationRequest
extends Omit<ApiNotificationRequestJSON, 'account' | 'notifications_count'> {
account_id: string;
notifications_count: number;
}
export function createNotificationRequestFromJSON(
requestJSON: ApiNotificationRequestJSON,
): NotificationRequest {
const { account, notifications_count, ...request } = requestJSON;
return {
account_id: account.id,
notifications_count: +notifications_count,
...request,
};
}

View file

@ -1,114 +0,0 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { blockAccountSuccess, muteAccountSuccess } from 'flavours/glitch/actions/accounts';
import {
NOTIFICATION_REQUESTS_EXPAND_REQUEST,
NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
NOTIFICATION_REQUESTS_EXPAND_FAIL,
NOTIFICATION_REQUESTS_FETCH_REQUEST,
NOTIFICATION_REQUESTS_FETCH_SUCCESS,
NOTIFICATION_REQUESTS_FETCH_FAIL,
NOTIFICATION_REQUEST_FETCH_REQUEST,
NOTIFICATION_REQUEST_FETCH_SUCCESS,
NOTIFICATION_REQUEST_FETCH_FAIL,
NOTIFICATION_REQUEST_ACCEPT_REQUEST,
NOTIFICATION_REQUEST_DISMISS_REQUEST,
NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
NOTIFICATION_REQUESTS_DISMISS_REQUEST,
NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
} from 'flavours/glitch/actions/notifications';
import { notificationToMap } from './notifications';
const initialState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
next: null,
current: ImmutableMap({
isLoading: false,
item: null,
removed: false,
notifications: ImmutableMap({
items: ImmutableList(),
isLoading: false,
next: null,
}),
}),
});
const normalizeRequest = request => fromJS({
...request,
account: request.account.id,
});
const removeRequest = (state, id) => {
if (state.getIn(['current', 'item', 'id']) === id) {
state = state.setIn(['current', 'removed'], true);
}
return state.update('items', list => list.filterNot(item => item.get('id') === id));
};
const removeRequestByAccount = (state, account_id) => {
if (state.getIn(['current', 'item', 'account']) === account_id) {
state = state.setIn(['current', 'removed'], true);
}
return state.update('items', list => list.filterNot(item => item.get('account') === account_id));
};
export const notificationRequestsReducer = (state = initialState, action) => {
switch(action.type) {
case NOTIFICATION_REQUESTS_FETCH_SUCCESS:
return state.withMutations(map => {
map.update('items', list => ImmutableList(action.requests.map(normalizeRequest)).concat(list));
map.set('isLoading', false);
map.update('next', next => next ?? action.next);
});
case NOTIFICATION_REQUESTS_EXPAND_SUCCESS:
return state.withMutations(map => {
map.update('items', list => list.concat(ImmutableList(action.requests.map(normalizeRequest))));
map.set('isLoading', false);
map.set('next', action.next);
});
case NOTIFICATION_REQUESTS_EXPAND_REQUEST:
case NOTIFICATION_REQUESTS_FETCH_REQUEST:
return state.set('isLoading', true);
case NOTIFICATION_REQUESTS_EXPAND_FAIL:
case NOTIFICATION_REQUESTS_FETCH_FAIL:
return state.set('isLoading', false);
case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
case NOTIFICATION_REQUEST_DISMISS_REQUEST:
return removeRequest(state, action.id);
case NOTIFICATION_REQUESTS_ACCEPT_REQUEST:
case NOTIFICATION_REQUESTS_DISMISS_REQUEST:
return action.ids.reduce((state, id) => removeRequest(state, id), state);
case blockAccountSuccess.type:
return removeRequestByAccount(state, action.payload.relationship.id);
case muteAccountSuccess.type:
return action.payload.relationship.muting_notifications ? removeRequestByAccount(state, action.payload.relationship.id) : state;
case NOTIFICATION_REQUEST_FETCH_REQUEST:
return state.set('current', initialState.get('current').set('isLoading', true));
case NOTIFICATION_REQUEST_FETCH_SUCCESS:
return state.update('current', map => map.set('isLoading', false).set('item', normalizeRequest(action.request)));
case NOTIFICATION_REQUEST_FETCH_FAIL:
return state.update('current', map => map.set('isLoading', false));
case NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST:
case NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST:
return state.setIn(['current', 'notifications', 'isLoading'], true);
case NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS:
return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => ImmutableList(action.notifications.map(notificationToMap)).concat(list)).update('next', next => next ?? action.next));
case NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS:
return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => list.concat(ImmutableList(action.notifications.map(notificationToMap)))).set('next', action.next));
case NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL:
case NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL:
return state.setIn(['current', 'notifications', 'isLoading'], false);
default:
return state;
}
};

View file

@ -0,0 +1,182 @@
import { createReducer, isAnyOf } from '@reduxjs/toolkit';
import {
blockAccountSuccess,
muteAccountSuccess,
} from 'flavours/glitch/actions/accounts';
import {
fetchNotificationRequests,
expandNotificationRequests,
fetchNotificationRequest,
fetchNotificationsForRequest,
expandNotificationsForRequest,
acceptNotificationRequest,
dismissNotificationRequest,
acceptNotificationRequests,
dismissNotificationRequests,
} from 'flavours/glitch/actions/notification_requests';
import type { NotificationRequest } from 'flavours/glitch/models/notification_request';
import { createNotificationRequestFromJSON } from 'flavours/glitch/models/notification_request';
import { notificationToMap } from './notifications';
interface NotificationsListState {
items: unknown[]; // TODO
isLoading: boolean;
next: string | null;
}
interface CurrentNotificationRequestState {
item: NotificationRequest | null;
isLoading: boolean;
removed: boolean;
notifications: NotificationsListState;
}
interface NotificationRequestsState {
items: NotificationRequest[];
isLoading: boolean;
next: string | null;
current: CurrentNotificationRequestState;
}
const initialState: NotificationRequestsState = {
items: [],
isLoading: false,
next: null,
current: {
item: null,
isLoading: false,
removed: false,
notifications: {
isLoading: false,
items: [],
next: null,
},
},
};
const removeRequest = (state: NotificationRequestsState, id: string) => {
if (state.current.item?.id === id) {
state.current.removed = true;
}
state.items = state.items.filter((item) => item.id !== id);
};
const removeRequestByAccount = (
state: NotificationRequestsState,
account_id: string,
) => {
if (state.current.item?.account_id === account_id) {
state.current.removed = true;
}
state.items = state.items.filter((item) => item.account_id !== account_id);
};
export const notificationRequestsReducer =
createReducer<NotificationRequestsState>(initialState, (builder) => {
builder
.addCase(fetchNotificationRequests.fulfilled, (state, action) => {
state.items = action.payload.requests
.map(createNotificationRequestFromJSON)
.concat(state.items);
state.isLoading = false;
state.next ??= action.payload.next ?? null;
})
.addCase(expandNotificationRequests.fulfilled, (state, action) => {
state.items = state.items.concat(
action.payload.requests.map(createNotificationRequestFromJSON),
);
state.isLoading = false;
state.next = action.payload.next ?? null;
})
.addCase(blockAccountSuccess, (state, action) => {
removeRequestByAccount(state, action.payload.relationship.id);
})
.addCase(muteAccountSuccess, (state, action) => {
if (action.payload.relationship.muting_notifications)
removeRequestByAccount(state, action.payload.relationship.id);
})
.addCase(fetchNotificationRequest.pending, (state) => {
state.current = { ...initialState.current, isLoading: true };
})
.addCase(fetchNotificationRequest.rejected, (state) => {
state.current.isLoading = false;
})
.addCase(fetchNotificationRequest.fulfilled, (state, action) => {
state.current.isLoading = false;
state.current.item = createNotificationRequestFromJSON(action.payload);
})
.addCase(fetchNotificationsForRequest.fulfilled, (state, action) => {
state.current.notifications.isLoading = false;
state.current.notifications.items.unshift(
...action.payload.notifications.map(notificationToMap),
);
state.current.notifications.next ??= action.payload.next ?? null;
})
.addCase(expandNotificationsForRequest.fulfilled, (state, action) => {
state.current.notifications.isLoading = false;
state.current.notifications.items.push(
...action.payload.notifications.map(notificationToMap),
);
state.current.notifications.next = action.payload.next ?? null;
})
.addMatcher(
isAnyOf(
fetchNotificationRequests.pending,
expandNotificationRequests.pending,
),
(state) => {
state.isLoading = true;
},
)
.addMatcher(
isAnyOf(
fetchNotificationRequests.rejected,
expandNotificationRequests.rejected,
),
(state) => {
state.isLoading = false;
},
)
.addMatcher(
isAnyOf(
acceptNotificationRequest.pending,
dismissNotificationRequest.pending,
),
(state, action) => {
removeRequest(state, action.meta.arg.id);
},
)
.addMatcher(
isAnyOf(
acceptNotificationRequests.pending,
dismissNotificationRequests.pending,
),
(state, action) => {
action.meta.arg.ids.forEach((id) => {
removeRequest(state, id);
});
},
)
.addMatcher(
isAnyOf(
fetchNotificationsForRequest.pending,
expandNotificationsForRequest.pending,
),
(state) => {
state.current.notifications.isLoading = true;
},
)
.addMatcher(
isAnyOf(
fetchNotificationsForRequest.rejected,
expandNotificationsForRequest.rejected,
),
(state) => {
state.current.notifications.isLoading = false;
},
);
});

View file

@ -55,11 +55,11 @@ const initialState = ImmutableMap({
markNewForDelete: false,
});
export const notificationToMap = (notification, markForDelete = false) => ImmutableMap({
export const notificationToMap = (notification) => ImmutableMap({
id: notification.id,
type: notification.type,
account: notification.account.id,
markedForDelete: markForDelete,
markedForDelete: false,
status: notification.status ? notification.status.id : null,
report: notification.report ? fromJS(notification.report) : null,
event: notification.event ? fromJS(notification.event) : null,
@ -76,7 +76,7 @@ const normalizeNotification = (state, notification, usePendingItems) => {
}
if (usePendingItems || !state.get('pendingItems').isEmpty()) {
return state.update('pendingItems', list => list.unshift(notificationToMap(notification, markNewForDelete))).update('unread', unread => unread + 1);
return state.update('pendingItems', list => list.unshift(notificationToMap(notification).set('markForDelete', markNewForDelete))).update('unread', unread => unread + 1);
}
if (shouldCountUnreadNotifications(state)) {
@ -90,7 +90,7 @@ const normalizeNotification = (state, notification, usePendingItems) => {
list = list.take(20);
}
return list.unshift(notificationToMap(notification, markNewForDelete));
return list.unshift(notificationToMap(notification).set('markForDelete', markNewForDelete));
});
};
@ -104,7 +104,7 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingMore
const markNewForDelete = state.get('markNewForDelete');
const lastReadId = state.get('lastReadId');
const newItems = ImmutableList(notifications.map((notification) => notificationToMap(notification, markNewForDelete)));
const newItems = ImmutableList(notifications.map((notification) => notificationToMap(notification).set('markForDelete', markNewForDelete)));
return state.withMutations(mutable => {
if (!newItems.isEmpty()) {

View file

@ -33,8 +33,12 @@ interface AppThunkConfig {
}
type AppThunkApi = Pick<GetThunkAPI<AppThunkConfig>, 'getState' | 'dispatch'>;
interface AppThunkOptions {
interface AppThunkOptions<Arg> {
useLoadingBar?: boolean;
condition?: (
arg: Arg,
{ getState }: { getState: AppThunkApi['getState'] },
) => boolean;
}
const createBaseAsyncThunk = createAsyncThunk.withTypes<AppThunkConfig>();
@ -42,7 +46,7 @@ const createBaseAsyncThunk = createAsyncThunk.withTypes<AppThunkConfig>();
export function createThunk<Arg = void, Returned = void>(
name: string,
creator: (arg: Arg, api: AppThunkApi) => Returned | Promise<Returned>,
options: AppThunkOptions = {},
options: AppThunkOptions<Arg> = {},
) {
return createBaseAsyncThunk(
name,
@ -70,6 +74,7 @@ export function createThunk<Arg = void, Returned = void>(
if (options.useLoadingBar) return { useLoadingBar: true };
return {};
},
condition: options.condition,
},
);
}
@ -96,7 +101,7 @@ type ArgsType = Record<string, unknown> | undefined;
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string,
loadData: (args: Args) => Promise<LoadDataResult>,
thunkOptions?: AppThunkOptions,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, LoadDataResult>>;
// Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty
@ -104,17 +109,19 @@ export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string,
loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?:
| AppThunkOptions
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, DiscardLoadData>,
thunkOptions?: AppThunkOptions,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, void>>;
// Overload when the `onData` method returns nothing, then the mayload is the `onData` result
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string,
loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: AppThunkOptions | OnData<Args, LoadDataResult, void>,
thunkOptions?: AppThunkOptions,
onDataOrThunkOptions?:
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, void>,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, LoadDataResult>>;
// Overload when there is an `onData` method returning something
@ -126,9 +133,9 @@ export function createDataLoadingThunk<
name: string,
loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?:
| AppThunkOptions
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, Returned>,
thunkOptions?: AppThunkOptions,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, Returned>>;
/**
@ -154,6 +161,7 @@ export function createDataLoadingThunk<
* @param maybeThunkOptions
* Additional Mastodon specific options for the thunk. Currently supports:
* - `useLoadingBar` to display a loading bar while this action is pending. Defaults to true.
* - `condition` is passed to `createAsyncThunk` (https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution)
* @returns The created thunk
*/
export function createDataLoadingThunk<
@ -164,12 +172,12 @@ export function createDataLoadingThunk<
name: string,
loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?:
| AppThunkOptions
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, Returned>,
maybeThunkOptions?: AppThunkOptions,
maybeThunkOptions?: AppThunkOptions<Args>,
) {
let onData: OnData<Args, LoadDataResult, Returned> | undefined;
let thunkOptions: AppThunkOptions | undefined;
let thunkOptions: AppThunkOptions<Args> | undefined;
if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions;
else if (typeof onDataOrThunkOptions === 'object')
@ -203,6 +211,9 @@ export function createDataLoadingThunk<
return undefined as Returned;
else return result;
},
{ useLoadingBar: thunkOptions?.useLoadingBar ?? true },
{
useLoadingBar: thunkOptions?.useLoadingBar ?? true,
condition: thunkOptions?.condition,
},
);
}

View file

@ -10761,6 +10761,7 @@ noscript {
scroll-padding: 16px;
scroll-behavior: smooth;
overflow-x: scroll;
scrollbar-width: none;
&__card {
background: var(--background-color);

View file

@ -2,7 +2,7 @@ import { createAction } from '@reduxjs/toolkit';
import {
apiClearNotifications,
apiFetchNotifications,
apiFetchNotificationGroups,
} from 'mastodon/api/notifications';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type {
@ -71,7 +71,7 @@ function dispatchAssociatedRecords(
export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) =>
apiFetchNotifications({ exclude_types: getExcludedTypes(getState()) }),
apiFetchNotificationGroups({ exclude_types: getExcludedTypes(getState()) }),
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
@ -92,7 +92,7 @@ export const fetchNotifications = createDataLoadingThunk(
export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }, { getState }) =>
apiFetchNotifications({
apiFetchNotificationGroups({
max_id: params.gap.maxId,
exclude_types: getExcludedTypes(getState()),
}),
@ -108,7 +108,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => {
return apiFetchNotifications({
return apiFetchNotificationGroups({
max_id: undefined,
exclude_types: getExcludedTypes(getState()),
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones

View file

@ -0,0 +1,234 @@
import {
apiFetchNotificationRequest,
apiFetchNotificationRequests,
apiFetchNotifications,
apiAcceptNotificationRequest,
apiDismissNotificationRequest,
apiAcceptNotificationRequests,
apiDismissNotificationRequests,
} from 'mastodon/api/notifications';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
} from 'mastodon/api_types/notifications';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
import type { AppDispatch, RootState } from 'mastodon/store';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { decreasePendingNotificationsCount } from './notification_policies';
// TODO: refactor with notification_groups
function dispatchAssociatedRecords(
dispatch: AppDispatch,
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];
notifications.forEach((notification) => {
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) {
fetchedStatuses.push(notification.status);
}
});
if (fetchedAccounts.length > 0)
dispatch(importFetchedAccounts(fetchedAccounts));
if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
}
export const fetchNotificationRequests = createDataLoadingThunk(
'notificationRequests/fetch',
async (_params, { getState }) => {
let sinceId = undefined;
if (getState().notificationRequests.items.length > 0) {
sinceId = getState().notificationRequests.items[0]?.id;
}
return apiFetchNotificationRequests({
since_id: sinceId,
});
},
({ requests, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatch(importFetchedAccounts(requests.map((request) => request.account)));
return { requests, next: next?.uri };
},
{
condition: (_params, { getState }) =>
!getState().notificationRequests.isLoading,
},
);
export const fetchNotificationRequest = createDataLoadingThunk(
'notificationRequest/fetch',
async ({ id }: { id: string }) => apiFetchNotificationRequest(id),
{
condition: ({ id }, { getState }) =>
!(
getState().notificationRequests.current.item?.id === id ||
getState().notificationRequests.current.isLoading
),
},
);
export const expandNotificationRequests = createDataLoadingThunk(
'notificationRequests/expand',
async (_, { getState }) => {
const nextUrl = getState().notificationRequests.next;
if (!nextUrl) throw new Error('missing URL');
return apiFetchNotificationRequests(undefined, nextUrl);
},
({ requests, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatch(importFetchedAccounts(requests.map((request) => request.account)));
return { requests, next: next?.uri };
},
{
condition: (_, { getState }) =>
!!getState().notificationRequests.next &&
!getState().notificationRequests.isLoading,
},
);
export const fetchNotificationsForRequest = createDataLoadingThunk(
'notificationRequest/fetchNotifications',
async ({ accountId }: { accountId: string }, { getState }) => {
const sinceId =
// @ts-expect-error current.notifications.items is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
getState().notificationRequests.current.notifications.items[0]?.get(
'id',
) as string | undefined;
return apiFetchNotifications({
since_id: sinceId,
account_id: accountId,
});
},
({ notifications, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatchAssociatedRecords(dispatch, notifications);
return { notifications, next: next?.uri };
},
{
condition: ({ accountId }, { getState }) => {
const current = getState().notificationRequests.current;
return !(
current.item?.account_id === accountId &&
current.notifications.isLoading
);
},
},
);
export const expandNotificationsForRequest = createDataLoadingThunk(
'notificationRequest/expandNotifications',
async (_, { getState }) => {
const nextUrl = getState().notificationRequests.current.notifications.next;
if (!nextUrl) throw new Error('missing URL');
return apiFetchNotifications(undefined, nextUrl);
},
({ notifications, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatchAssociatedRecords(dispatch, notifications);
return { notifications, next: next?.uri };
},
{
condition: ({ accountId }: { accountId: string }, { getState }) => {
const url = getState().notificationRequests.current.notifications.next;
return (
!!url &&
!getState().notificationRequests.current.notifications.isLoading &&
getState().notificationRequests.current.item?.account_id === accountId
);
},
},
);
const selectNotificationCountForRequest = (state: RootState, id: string) => {
const requests = state.notificationRequests.items;
const thisRequest = requests.find((request) => request.id === id);
return thisRequest ? thisRequest.notifications_count : 0;
};
export const acceptNotificationRequest = createDataLoadingThunk(
'notificationRequest/accept',
({ id }: { id: string }) => apiAcceptNotificationRequest(id),
(_data, { dispatch, getState, discardLoadData, actionArg: { id } }) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(decreasePendingNotificationsCount(count));
// The payload is not used in any functions
return discardLoadData;
},
);
export const dismissNotificationRequest = createDataLoadingThunk(
'notificationRequest/dismiss',
({ id }: { id: string }) => apiDismissNotificationRequest(id),
(_data, { dispatch, getState, discardLoadData, actionArg: { id } }) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(decreasePendingNotificationsCount(count));
// The payload is not used in any functions
return discardLoadData;
},
);
export const acceptNotificationRequests = createDataLoadingThunk(
'notificationRequests/acceptBulk',
({ ids }: { ids: string[] }) => apiAcceptNotificationRequests(ids),
(_data, { dispatch, getState, discardLoadData, actionArg: { ids } }) => {
const count = ids.reduce(
(count, id) => count + selectNotificationCountForRequest(getState(), id),
0,
);
dispatch(decreasePendingNotificationsCount(count));
// The payload is not used in any functions
return discardLoadData;
},
);
export const dismissNotificationRequests = createDataLoadingThunk(
'notificationRequests/dismissBulk',
({ ids }: { ids: string[] }) => apiDismissNotificationRequests(ids),
(_data, { dispatch, getState, discardLoadData, actionArg: { ids } }) => {
const count = ids.reduce(
(count, id) => count + selectNotificationCountForRequest(getState(), id),
0,
);
dispatch(decreasePendingNotificationsCount(count));
// The payload is not used in any functions
return discardLoadData;
},
);

View file

@ -18,7 +18,6 @@ import {
importFetchedStatuses,
} from './importer';
import { submitMarkers } from './markers';
import { decreasePendingNotificationsCount } from './notification_policies';
import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings';
@ -44,26 +43,6 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST';
export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS';
export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL';
export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST';
export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS';
export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL';
export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST';
export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL';
export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST';
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';
@ -72,14 +51,6 @@ export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISM
export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL';
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
@ -93,12 +64,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
}
};
const selectNotificationCountForRequest = (state, id) => {
const requests = state.getIn(['notificationRequests', 'items']);
const thisRequest = requests.find(request => request.get('id') === id);
return thisRequest ? thisRequest.get('notifications_count') : 0;
};
export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING,
});
@ -343,296 +308,3 @@ export function setBrowserPermission (value) {
value,
};
}
export const fetchNotificationRequests = () => (dispatch, getState) => {
const params = {};
if (getState().getIn(['notificationRequests', 'isLoading'])) {
return;
}
if (getState().getIn(['notificationRequests', 'items'])?.size > 0) {
params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']);
}
dispatch(fetchNotificationRequestsRequest());
api().get('/api/v1/notifications/requests', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchNotificationRequestsFail(err));
});
};
export const fetchNotificationRequestsRequest = () => ({
type: NOTIFICATION_REQUESTS_FETCH_REQUEST,
});
export const fetchNotificationRequestsSuccess = (requests, next) => ({
type: NOTIFICATION_REQUESTS_FETCH_SUCCESS,
requests,
next,
});
export const fetchNotificationRequestsFail = error => ({
type: NOTIFICATION_REQUESTS_FETCH_FAIL,
error,
});
export const expandNotificationRequests = () => (dispatch, getState) => {
const url = getState().getIn(['notificationRequests', 'next']);
if (!url || getState().getIn(['notificationRequests', 'isLoading'])) {
return;
}
dispatch(expandNotificationRequestsRequest());
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(expandNotificationRequestsSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(expandNotificationRequestsFail(err));
});
};
export const expandNotificationRequestsRequest = () => ({
type: NOTIFICATION_REQUESTS_EXPAND_REQUEST,
});
export const expandNotificationRequestsSuccess = (requests, next) => ({
type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
requests,
next,
});
export const expandNotificationRequestsFail = error => ({
type: NOTIFICATION_REQUESTS_EXPAND_FAIL,
error,
});
export const fetchNotificationRequest = id => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
if (current.getIn(['item', 'id']) === id || current.get('isLoading')) {
return;
}
dispatch(fetchNotificationRequestRequest(id));
api().get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
dispatch(fetchNotificationRequestSuccess(data));
}).catch(err => {
dispatch(fetchNotificationRequestFail(id, err));
});
};
export const fetchNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_FETCH_REQUEST,
id,
});
export const fetchNotificationRequestSuccess = request => ({
type: NOTIFICATION_REQUEST_FETCH_SUCCESS,
request,
});
export const fetchNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_FETCH_FAIL,
id,
error,
});
export const acceptNotificationRequest = (id) => (dispatch, getState) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(acceptNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
dispatch(acceptNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(id, err));
});
};
export const acceptNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_ACCEPT_REQUEST,
id,
});
export const acceptNotificationRequestSuccess = id => ({
type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS,
id,
});
export const acceptNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_ACCEPT_FAIL,
id,
error,
});
export const dismissNotificationRequest = (id) => (dispatch, getState) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(dismissNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
dispatch(dismissNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(id, err));
});
};
export const dismissNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_DISMISS_REQUEST,
id,
});
export const dismissNotificationRequestSuccess = id => ({
type: NOTIFICATION_REQUEST_DISMISS_SUCCESS,
id,
});
export const dismissNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_DISMISS_FAIL,
id,
error,
});
export const acceptNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => {
dispatch(acceptNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(ids, err));
});
};
export const acceptNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
ids,
});
export const acceptNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS,
ids,
});
export const acceptNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_ACCEPT_FAIL,
ids,
error,
});
export const dismissNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => {
dispatch(dismissNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(ids, err));
});
};
export const dismissNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_REQUEST,
ids,
});
export const dismissNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS,
ids,
});
export const dismissNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_DISMISS_FAIL,
ids,
error,
});
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
const params = { account_id: accountId };
if (current.getIn(['item', 'account']) === accountId) {
if (current.getIn(['notifications', 'isLoading'])) {
return;
}
if (current.getIn(['notifications', 'items'])?.size > 0) {
params.since_id = current.getIn(['notifications', 'items', 0, 'id']);
}
}
dispatch(fetchNotificationsForRequestRequest());
api().get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(fetchNotificationsForRequestFail(err));
});
};
export const fetchNotificationsForRequestRequest = () => ({
type: NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
});
export const fetchNotificationsForRequestSuccess = (notifications, next) => ({
type: NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
notifications,
next,
});
export const fetchNotificationsForRequestFail = (error) => ({
type: NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
error,
});
export const expandNotificationsForRequest = () => (dispatch, getState) => {
const url = getState().getIn(['notificationRequests', 'current', 'notifications', 'next']);
if (!url || getState().getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])) {
return;
}
dispatch(expandNotificationsForRequestRequest());
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(expandNotificationsForRequestFail(err));
});
};
export const expandNotificationsForRequestRequest = () => ({
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
});
export const expandNotificationsForRequestSuccess = (notifications, next) => ({
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
notifications,
next,
});
export const expandNotificationsForRequestFail = (error) => ({
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
error,
});

View file

@ -1,14 +1,43 @@
import api, { apiRequest, getLinks } from 'mastodon/api';
import type { ApiNotificationGroupsResultJSON } from 'mastodon/api_types/notifications';
import api, {
apiRequest,
getLinks,
apiRequestGet,
apiRequestPost,
} from 'mastodon/api';
import type {
ApiNotificationGroupsResultJSON,
ApiNotificationRequestJSON,
ApiNotificationJSON,
} from 'mastodon/api_types/notifications';
export const apiFetchNotifications = async (params?: {
export const apiFetchNotifications = async (
params?: {
account_id?: string;
since_id?: string;
},
url?: string,
) => {
const response = await api().request<ApiNotificationJSON[]>({
method: 'GET',
url: url ?? '/api/v1/notifications',
params,
});
return {
notifications: response.data,
links: getLinks(response),
};
};
export const apiFetchNotificationGroups = async (params?: {
url?: string;
exclude_types?: string[];
max_id?: string;
since_id?: string;
}) => {
const response = await api().request<ApiNotificationGroupsResultJSON>({
method: 'GET',
url: '/api/v2_alpha/notifications',
url: '/api/v2/notifications',
params,
});
@ -24,3 +53,43 @@ export const apiFetchNotifications = async (params?: {
export const apiClearNotifications = () =>
apiRequest<undefined>('POST', 'v1/notifications/clear');
export const apiFetchNotificationRequests = async (
params?: {
since_id?: string;
},
url?: string,
) => {
const response = await api().request<ApiNotificationRequestJSON[]>({
method: 'GET',
url: url ?? '/api/v1/notifications/requests',
params,
});
return {
requests: response.data,
links: getLinks(response),
};
};
export const apiFetchNotificationRequest = async (id: string) => {
return apiRequestGet<ApiNotificationRequestJSON>(
`v1/notifications/requests/${id}`,
);
};
export const apiAcceptNotificationRequest = async (id: string) => {
return apiRequestPost(`v1/notifications/requests/${id}/accept`);
};
export const apiDismissNotificationRequest = async (id: string) => {
return apiRequestPost(`v1/notifications/requests/${id}/dismiss`);
};
export const apiAcceptNotificationRequests = async (id: string[]) => {
return apiRequestPost('v1/notifications/requests/accept', { id });
};
export const apiDismissNotificationRequests = async (id: string[]) => {
return apiRequestPost('v1/notifications/dismiss/dismiss', { id });
};

View file

@ -149,3 +149,12 @@ export interface ApiNotificationGroupsResultJSON {
statuses: ApiStatusJSON[];
notification_groups: ApiNotificationGroupJSON[];
}
export interface ApiNotificationRequestJSON {
id: string;
created_at: string;
updated_at: string;
notifications_count: string;
account: ApiAccountJSON;
last_status?: ApiStatusJSON;
}

View file

@ -12,7 +12,7 @@ import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { initBlockModal } from 'mastodon/actions/blocks';
import { initMuteModal } from 'mastodon/actions/mutes';
import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notification_requests';
import { initReport } from 'mastodon/actions/reports';
import { Avatar } from 'mastodon/components/avatar';
import { CheckBox } from 'mastodon/components/check_box';
@ -40,11 +40,11 @@ export const NotificationRequest = ({ id, accountId, notificationsCount, checked
const { push: historyPush } = useHistory();
const handleDismiss = useCallback(() => {
dispatch(dismissNotificationRequest(id));
dispatch(dismissNotificationRequest({ id }));
}, [dispatch, id]);
const handleAccept = useCallback(() => {
dispatch(acceptNotificationRequest(id));
dispatch(acceptNotificationRequest({ id }));
}, [dispatch, id]);
const handleMute = useCallback(() => {

View file

@ -10,7 +10,13 @@ import { useSelector, useDispatch } from 'react-redux';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
import { fetchNotificationRequest, fetchNotificationsForRequest, expandNotificationsForRequest, acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
import {
fetchNotificationRequest,
fetchNotificationsForRequest,
expandNotificationsForRequest,
acceptNotificationRequest,
dismissNotificationRequest,
} from 'mastodon/actions/notification_requests';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import { IconButton } from 'mastodon/components/icon_button';
@ -44,28 +50,28 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
const columnRef = useRef();
const intl = useIntl();
const dispatch = useDispatch();
const notificationRequest = useSelector(state => state.getIn(['notificationRequests', 'current', 'item', 'id']) === id ? state.getIn(['notificationRequests', 'current', 'item']) : null);
const accountId = notificationRequest?.get('account');
const notificationRequest = useSelector(state => state.notificationRequests.current.item?.id === id ? state.notificationRequests.current.item : null);
const accountId = notificationRequest?.account_id;
const account = useSelector(state => state.getIn(['accounts', accountId]));
const notifications = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'items']));
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'isLoading']));
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'current', 'notifications', 'next']));
const removed = useSelector(state => state.getIn(['notificationRequests', 'current', 'removed']));
const notifications = useSelector(state => state.notificationRequests.current.notifications.items);
const isLoading = useSelector(state => state.notificationRequests.current.notifications.isLoading);
const hasMore = useSelector(state => !!state.notificationRequests.current.notifications.next);
const removed = useSelector(state => state.notificationRequests.current.removed);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, [columnRef]);
const handleLoadMore = useCallback(() => {
dispatch(expandNotificationsForRequest());
}, [dispatch]);
dispatch(expandNotificationsForRequest({ accountId }));
}, [dispatch, accountId]);
const handleDismiss = useCallback(() => {
dispatch(dismissNotificationRequest(id));
dispatch(dismissNotificationRequest({ id }));
}, [dispatch, id]);
const handleAccept = useCallback(() => {
dispatch(acceptNotificationRequest(id));
dispatch(acceptNotificationRequest({ id }));
}, [dispatch, id]);
const handleMoveUp = useCallback(id => {
@ -79,12 +85,12 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
}, [columnRef, notifications]);
useEffect(() => {
dispatch(fetchNotificationRequest(id));
dispatch(fetchNotificationRequest({ id }));
}, [dispatch, id]);
useEffect(() => {
if (accountId) {
dispatch(fetchNotificationsForRequest(accountId));
dispatch(fetchNotificationsForRequest({ accountId }));
}
}, [dispatch, accountId]);

View file

@ -11,7 +11,12 @@ import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?rea
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'mastodon/actions/modal';
import { fetchNotificationRequests, expandNotificationRequests, acceptNotificationRequests, dismissNotificationRequests } from 'mastodon/actions/notifications';
import {
fetchNotificationRequests,
expandNotificationRequests,
acceptNotificationRequests,
dismissNotificationRequests,
} from 'mastodon/actions/notification_requests';
import { changeSetting } from 'mastodon/actions/settings';
import { CheckBox } from 'mastodon/components/check_box';
import Column from 'mastodon/components/column';
@ -84,7 +89,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
message: intl.formatMessage(messages.confirmAcceptMultipleMessage, { count: selectedItems.length }),
confirm: intl.formatMessage(messages.confirmAcceptMultipleButton, { count: selectedItems.length}),
onConfirm: () =>
dispatch(acceptNotificationRequests(selectedItems)),
dispatch(acceptNotificationRequests({ ids: selectedItems })),
},
}));
}, [dispatch, intl, selectedItems]);
@ -97,7 +102,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
message: intl.formatMessage(messages.confirmDismissMultipleMessage, { count: selectedItems.length }),
confirm: intl.formatMessage(messages.confirmDismissMultipleButton, { count: selectedItems.length}),
onConfirm: () =>
dispatch(dismissNotificationRequests(selectedItems)),
dispatch(dismissNotificationRequests({ ids: selectedItems })),
},
}));
}, [dispatch, intl, selectedItems]);
@ -161,9 +166,9 @@ export const NotificationRequests = ({ multiColumn }) => {
const columnRef = useRef();
const intl = useIntl();
const dispatch = useDispatch();
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'isLoading']));
const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next']));
const isLoading = useSelector(state => state.notificationRequests.isLoading);
const notificationRequests = useSelector(state => state.notificationRequests.items);
const hasMore = useSelector(state => !!state.notificationRequests.next);
const [selectionMode, setSelectionMode] = useState(false);
const [checkedRequestIds, setCheckedRequestIds] = useState([]);
@ -182,7 +187,7 @@ export const NotificationRequests = ({ multiColumn }) => {
else
ids.push(id);
setSelectAllChecked(ids.length === notificationRequests.size);
setSelectAllChecked(ids.length === notificationRequests.length);
return [...ids];
});
@ -193,7 +198,7 @@ export const NotificationRequests = ({ multiColumn }) => {
if(checked)
setCheckedRequestIds([]);
else
setCheckedRequestIds(notificationRequests.map(request => request.get('id')).toArray());
setCheckedRequestIds(notificationRequests.map(request => request.id));
return !checked;
});
@ -217,7 +222,7 @@ export const NotificationRequests = ({ multiColumn }) => {
multiColumn={multiColumn}
showBackButton
appendContent={
notificationRequests.size > 0 && (
notificationRequests.length > 0 && (
<SelectRow selectionMode={selectionMode} setSelectionMode={setSelectionMode} selectAllChecked={selectAllChecked} toggleSelectAll={toggleSelectAll} selectedItems={checkedRequestIds} />
)}
>
@ -236,12 +241,12 @@ export const NotificationRequests = ({ multiColumn }) => {
>
{notificationRequests.map(request => (
<NotificationRequest
key={request.get('id')}
id={request.get('id')}
accountId={request.get('account')}
notificationsCount={request.get('notifications_count')}
key={request.id}
id={request.id}
accountId={request.account_id}
notificationsCount={request.notifications_count}
showCheckbox={selectionMode}
checked={checkedRequestIds.includes(request.get('id'))}
checked={checkedRequestIds.includes(request.id)}
toggleCheck={handleCheck}
/>
))}

View file

@ -789,6 +789,7 @@
"status.edit": "Edita",
"status.edited": "Darrera edició {date}",
"status.edited_x_times": "Editat {count, plural, one {{count} vegada} other {{count} vegades}}",
"status.embed": "Obté el codi encastat",
"status.favourite": "Favorit",
"status.favourites": "{count, plural, one {favorit} other {favorits}}",
"status.filter": "Filtra aquest tut",

View file

@ -789,6 +789,7 @@
"status.edit": "Edit",
"status.edited": "Last edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Get embed code",
"status.favourite": "Favourite",
"status.favourites": "{count, plural, one {favorite} other {favorites}}",
"status.filter": "Filter this post",

View file

@ -789,6 +789,7 @@
"status.edit": "Editar",
"status.edited": "Última edición: {date}",
"status.edited_x_times": "Editado {count, plural, one {{count} vez} other {{count} veces}}",
"status.embed": "Obtener código para insertar",
"status.favourite": "Marcar como favorito",
"status.favourites": "{count, plural, one {# voto} other {# votos}}",
"status.filter": "Filtrar este mensaje",

View file

@ -376,7 +376,7 @@
"ignore_notifications_modal.ignore": "Ignorar notificaciones",
"ignore_notifications_modal.limited_accounts_title": "¿Ignorar notificaciones de cuentas moderadas?",
"ignore_notifications_modal.new_accounts_title": "¿Ignorar notificaciones de cuentas nuevas?",
"ignore_notifications_modal.not_followers_title": "¿Ignorar notificaciones de personas que no te sigue?",
"ignore_notifications_modal.not_followers_title": "¿Ignorar notificaciones de personas que no te siguen?",
"ignore_notifications_modal.not_following_title": "¿Ignorar notificaciones de personas a las que no sigues?",
"ignore_notifications_modal.private_mentions_title": "¿Ignorar notificaciones de menciones privadas no solicitadas?",
"interaction_modal.description.favourite": "Con una cuenta en Mastodon, puedes marcar como favorita esta publicación para que el autor sepa que te gusta, y guardala para más adelante.",
@ -789,6 +789,7 @@
"status.edit": "Editar",
"status.edited": "Última edición {date}",
"status.edited_x_times": "Editado {count, plural, one {{count} time} other {{count} veces}}",
"status.embed": "Obtener código para incrustar",
"status.favourite": "Favorito",
"status.favourites": "{count, plural, one {favorito} other {favoritos}}",
"status.filter": "Filtrar esta publicación",

View file

@ -376,7 +376,7 @@
"ignore_notifications_modal.ignore": "Ignorar notificaciones",
"ignore_notifications_modal.limited_accounts_title": "¿Ignorar notificaciones de cuentas moderadas?",
"ignore_notifications_modal.new_accounts_title": "¿Ignorar notificaciones de cuentas nuevas?",
"ignore_notifications_modal.not_followers_title": "¿Ignorar notificaciones de personas que no te sigue?",
"ignore_notifications_modal.not_followers_title": "¿Ignorar notificaciones de personas que no te siguen?",
"ignore_notifications_modal.not_following_title": "¿Ignorar notificaciones de personas a las que no sigues?",
"ignore_notifications_modal.private_mentions_title": "¿Ignorar notificaciones de menciones privadas no solicitadas?",
"interaction_modal.description.favourite": "Con una cuenta en Mastodon, puedes marcar como favorita esta publicación para que el autor sepa que te gusta, y guardala para más adelante.",
@ -789,6 +789,7 @@
"status.edit": "Editar",
"status.edited": "Última edición {date}",
"status.edited_x_times": "Editado {count, plural, one {{count} vez} other {{count} veces}}",
"status.embed": "Obtener código para incrustar",
"status.favourite": "Favorito",
"status.favourites": "{count, plural, one {favorito} other {favoritos}}",
"status.filter": "Filtrar esta publicación",

View file

@ -457,6 +457,7 @@
"lists.subheading": "Sinu nimekirjad",
"load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
"loading_indicator.label": "Laadimine…",
"media_gallery.hide": "Peida",
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
"mute_modal.hide_from_notifications": "Peida teavituste hulgast",
"mute_modal.hide_options": "Peida valikud",
@ -779,6 +780,7 @@
"status.bookmark": "Järjehoidja",
"status.cancel_reblog_private": "Lõpeta jagamine",
"status.cannot_reblog": "Seda postitust ei saa jagada",
"status.continued_thread": "Jätkatud lõim",
"status.copy": "Kopeeri postituse link",
"status.delete": "Kustuta",
"status.detailed_status": "Detailne vestluskuva",
@ -787,6 +789,7 @@
"status.edit": "Muuda",
"status.edited": "Viimati muudetud {date}",
"status.edited_x_times": "Muudetud {count, plural, one{{count} kord} other {{count} korda}}",
"status.embed": "Hangi manustamiskood",
"status.favourite": "Lemmik",
"status.favourites": "{count, plural, one {lemmik} other {lemmikud}}",
"status.filter": "Filtreeri seda postitust",
@ -811,6 +814,7 @@
"status.reblogs.empty": "Keegi pole seda postitust veel jaganud. Kui keegi seda teeb, näeb seda siin.",
"status.redraft": "Kustuta & alga uuesti",
"status.remove_bookmark": "Eemalda järjehoidja",
"status.replied_in_thread": "Vastatud lõimes",
"status.replied_to": "Vastas kasutajale {name}",
"status.reply": "Vasta",
"status.replyAll": "Vasta lõimele",

View file

@ -457,6 +457,7 @@
"lists.subheading": "Zure zerrendak",
"load_pending": "{count, plural, one {elementu berri #} other {# elementu berri}}",
"loading_indicator.label": "Kargatzen…",
"media_gallery.hide": "Ezkutatu",
"moved_to_account_banner.text": "Zure {disabledAccount} kontua desgaituta dago une honetan, {movedToAccount} kontura aldatu zinelako.",
"mute_modal.hide_from_notifications": "Ezkutatu jakinarazpenetatik",
"mute_modal.hide_options": "Ezkutatu aukerak",
@ -775,6 +776,7 @@
"status.bookmark": "Laster-marka",
"status.cancel_reblog_private": "Kendu bultzada",
"status.cannot_reblog": "Bidalketa honi ezin zaio bultzada eman",
"status.continued_thread": "Harian jarraitu zuen",
"status.copy": "Kopiatu bidalketaren esteka",
"status.delete": "Ezabatu",
"status.detailed_status": "Elkarrizketaren ikuspegi xehetsua",
@ -783,6 +785,7 @@
"status.edit": "Editatu",
"status.edited": "Azken edizioa: {date}",
"status.edited_x_times": "{count, plural, one {behin} other {{count} aldiz}} editatua",
"status.embed": "Lortu txertatzeko kodea",
"status.favourite": "Gogokoa",
"status.favourites": "{count, plural, one {gogoko} other {gogoko}}",
"status.filter": "Iragazi bidalketa hau",
@ -807,6 +810,7 @@
"status.reblogs.empty": "Inork ez dio bultzada eman bidalketa honi oraindik. Inork egiten badu, hemen agertuko da.",
"status.redraft": "Ezabatu eta berridatzi",
"status.remove_bookmark": "Kendu laster-marka",
"status.replied_in_thread": "Harian erantzun zuen",
"status.replied_to": "{name} erabiltzaileari erantzuna",
"status.reply": "Erantzun",
"status.replyAll": "Erantzun harian",

View file

@ -789,6 +789,7 @@
"status.edit": "Rætta",
"status.edited": "Seinast broytt {date}",
"status.edited_x_times": "Rættað {count, plural, one {{count} ferð} other {{count} ferð}}",
"status.embed": "Fá kodu at seta inn",
"status.favourite": "Dámdur postur",
"status.favourites": "{count, plural, one {yndispostur} other {yndispostar}}",
"status.filter": "Filtrera hendan postin",

View file

@ -36,6 +36,7 @@
"account.followers.empty": "Personne ne suit ce compte pour l'instant.",
"account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}",
"account.following": "Abonné·e",
"account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}",
"account.follows.empty": "Ce compte ne suit personne présentement.",
"account.go_to_profile": "Voir ce profil",
"account.hide_reblogs": "Masquer les boosts de @{name}",
@ -788,6 +789,7 @@
"status.edit": "Modifier",
"status.edited": "Dernière modification le {date}",
"status.edited_x_times": "Modifiée {count, plural, one {{count} fois} other {{count} fois}}",
"status.embed": "Obtenir le code d'intégration",
"status.favourite": "Ajouter aux favoris",
"status.favourites": "{count, plural, one {favori} other {favoris}}",
"status.filter": "Filtrer cette publication",
@ -812,6 +814,7 @@
"status.reblogs.empty": "Personne na encore boosté cette publication. Lorsque quelquun le fera, elle apparaîtra ici.",
"status.redraft": "Supprimer et réécrire",
"status.remove_bookmark": "Retirer des signets",
"status.replied_in_thread": "A répondu dans un fil de discussion",
"status.replied_to": "A répondu à {name}",
"status.reply": "Répondre",
"status.replyAll": "Répondre à cette discussion",

View file

@ -36,6 +36,7 @@
"account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour linstant.",
"account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}",
"account.following": "Abonnements",
"account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}",
"account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour linstant.",
"account.go_to_profile": "Aller au profil",
"account.hide_reblogs": "Masquer les partages de @{name}",
@ -788,6 +789,7 @@
"status.edit": "Modifier",
"status.edited": "Dernière modification le {date}",
"status.edited_x_times": "Modifié {count, plural, one {{count} fois} other {{count} fois}}",
"status.embed": "Obtenir le code d'intégration",
"status.favourite": "Ajouter aux favoris",
"status.favourites": "{count, plural, one {favori} other {favoris}}",
"status.filter": "Filtrer ce message",
@ -812,6 +814,7 @@
"status.reblogs.empty": "Personne na encore partagé ce message. Lorsque quelquun le fera, il apparaîtra ici.",
"status.redraft": "Supprimer et réécrire",
"status.remove_bookmark": "Retirer des marque-pages",
"status.replied_in_thread": "A répondu dans un fil de discussion",
"status.replied_to": "En réponse à {name}",
"status.reply": "Répondre",
"status.replyAll": "Répondre au fil",

View file

@ -457,6 +457,7 @@
"lists.subheading": "Do liostaí",
"load_pending": "{count, plural, one {# mír nua} two {# mír nua} few {# mír nua} many {# mír nua} other {# mír nua}}",
"loading_indicator.label": "Á lódáil…",
"media_gallery.hide": "Folaigh",
"moved_to_account_banner.text": "Tá do chuntas {disabledAccount} díchumasaithe faoi láthair toisc gur bhog tú go {movedToAccount}.",
"mute_modal.hide_from_notifications": "Folaigh ó fhógraí",
"mute_modal.hide_options": "Folaigh roghanna",
@ -779,6 +780,7 @@
"status.bookmark": "Leabharmharcanna",
"status.cancel_reblog_private": "Dímhol",
"status.cannot_reblog": "Ní féidir an phostáil seo a mholadh",
"status.continued_thread": "Snáithe ar lean",
"status.copy": "Cóipeáil an nasc chuig an bpostáil",
"status.delete": "Scrios",
"status.detailed_status": "Amharc comhrá mionsonraithe",
@ -787,6 +789,7 @@
"status.edit": "Cuir in eagar",
"status.edited": "Arna chuir in eagar anuas {date}",
"status.edited_x_times": "Curtha in eagar {count, plural, one {{count} uair amháin} two {{count} uair} few {{count} uair} many {{count} uair} other {{count} uair}}",
"status.embed": "Faigh cód leabú",
"status.favourite": "Is fearr leat",
"status.favourites": "{count, plural, one {a bhfuil grá agat do} two {gráite} few {gráite} many {gráite} other {gráite}}",
"status.filter": "Déan scagadh ar an bpostáil seo",
@ -811,6 +814,7 @@
"status.reblogs.empty": "Níor mhol éinne an phostáil seo fós. Nuair a mholfaidh duine éigin í, taispeánfar anseo é sin.",
"status.redraft": "Scrios ⁊ athdhréachtaigh",
"status.remove_bookmark": "Bain leabharmharc",
"status.replied_in_thread": "D'fhreagair sa snáithe",
"status.replied_to": "D'fhreagair {name}",
"status.reply": "Freagair",
"status.replyAll": "Freagair le snáithe",

View file

@ -457,6 +457,7 @@
"lists.subheading": "Na liostaichean agad",
"load_pending": "{count, plural, one {# nì ùr} two {# nì ùr} few {# nithean ùra} other {# nì ùr}}",
"loading_indicator.label": "Ga luchdadh…",
"media_gallery.hide": "Falaich",
"moved_to_account_banner.text": "Tha an cunntas {disabledAccount} agad à comas on a rinn thu imrich gu {movedToAccount}.",
"mute_modal.hide_from_notifications": "Falaich o na brathan",
"mute_modal.hide_options": "Roghainnean falaich",
@ -779,6 +780,7 @@
"status.bookmark": "Cuir ris na comharran-lìn",
"status.cancel_reblog_private": "Na brosnaich tuilleadh",
"status.cannot_reblog": "Cha ghabh am post seo brosnachadh",
"status.continued_thread": "Pàirt de shnàithlean",
"status.copy": "Dèan lethbhreac dhen cheangal dhan phost",
"status.delete": "Sguab às",
"status.detailed_status": "Mion-shealladh a chòmhraidh",
@ -787,6 +789,7 @@
"status.edit": "Deasaich",
"status.edited": "An deasachadh mu dheireadh {date}",
"status.edited_x_times": "Chaidh a dheasachadh {count, plural, one {{count} turas} two {{count} thuras} few {{count} tursan} other {{count} turas}}",
"status.embed": "Faigh còd leabachaidh",
"status.favourite": "Cuir ris na h-annsachdan",
"status.favourites": "{count, plural, one {annsachd} two {annsachd} few {annsachdan} other {annsachd}}",
"status.filter": "Criathraich am post seo",
@ -811,6 +814,7 @@
"status.reblogs.empty": "Chan deach am post seo a bhrosnachadh le duine sam bith fhathast. Nuair a bhrosnaicheas cuideigin e, nochdaidh iad an-seo.",
"status.redraft": "Sguab às ⁊ dèan dreachd ùr",
"status.remove_bookmark": "Thoir an comharra-lìn air falbh",
"status.replied_in_thread": "Freagairt do shnàithlean",
"status.replied_to": "Air {name} fhreagairt",
"status.reply": "Freagair",
"status.replyAll": "Freagair dhan t-snàithlean",

View file

@ -355,6 +355,7 @@
"hints.profiles.see_more_followers": "Vider plus de sequitores sur {domain}",
"hints.profiles.see_more_follows": "Vider plus de sequites sur {domain}",
"hints.profiles.see_more_posts": "Vider plus de messages sur {domain}",
"hints.threads.replies_may_be_missing": "Responsas de altere servitores pote esser perdite.",
"hints.threads.see_more": "Vider plus de responsas sur {domain}",
"home.column_settings.show_reblogs": "Monstrar impulsos",
"home.column_settings.show_replies": "Monstrar responsas",

View file

@ -789,6 +789,7 @@
"status.edit": "Breyta",
"status.edited": "Síðast breytt {date}",
"status.edited_x_times": "Breytt {count, plural, one {{count} sinni} other {{count} sinnum}}",
"status.embed": "Ná í innfellanlegan kóða",
"status.favourite": "Eftirlæti",
"status.favourites": "{count, plural, one {eftirlæti} other {eftirlæti}}",
"status.filter": "Sía þessa færslu",

View file

@ -371,6 +371,14 @@
"ignore_notifications_modal.disclaimer": "O Mastodon não pode informar utilizadores que ignoraste as notificações deles. Ignorar notificações não irá parar as mensagens serem enviadas.",
"ignore_notifications_modal.filter_instead": "Filtrar em vez disso",
"ignore_notifications_modal.filter_to_act_users": "Ainda poderá aceitar, rejeitar, ou reportar utilizadores",
"ignore_notifications_modal.filter_to_avoid_confusion": "A filtragem ajuda a evitar potenciais equívocos",
"ignore_notifications_modal.filter_to_review_separately": "Pode rever as notificações filtradas separadamente",
"ignore_notifications_modal.ignore": "Ignorar notificações",
"ignore_notifications_modal.limited_accounts_title": "Ignorar notificações de contas moderadas?",
"ignore_notifications_modal.new_accounts_title": "Ignorar notificações de contas novas?",
"ignore_notifications_modal.not_followers_title": "Ignorar notificações de pessoas que não o seguem?",
"ignore_notifications_modal.not_following_title": "Ignorar notificações de pessoas que não segue?",
"ignore_notifications_modal.private_mentions_title": "Ignorar notificações de Menções Privadas não solicitadas?",
"interaction_modal.description.favourite": "Com uma conta no Mastodon, pode adicionar assinalar esta publicação como favorita para que o autor saiba que gostou e guardá-la para mais tarde.",
"interaction_modal.description.follow": "Com uma conta no Mastodon, pode seguir {name} para receber as suas publicações na sua página inicial.",
"interaction_modal.description.reblog": "Com uma conta no Mastodon, pode impulsionar esta publicação para compartilhá-lo com os seus seguidores.",
@ -449,6 +457,7 @@
"lists.subheading": "As tuas listas",
"load_pending": "{count, plural, one {# novo item} other {# novos itens}}",
"loading_indicator.label": "A carregar…",
"media_gallery.hide": "Esconder",
"moved_to_account_banner.text": "A sua conta {disabledAccount} está, no momento, desativada, porque você migrou para {movedToAccount}.",
"mute_modal.hide_from_notifications": "Ocultar das notificações",
"mute_modal.hide_options": "Ocultar opções",
@ -460,6 +469,7 @@
"mute_modal.you_wont_see_mentions": "Não verá publicações que os mencionem.",
"mute_modal.you_wont_see_posts": "Eles podem continuar a ver as suas publicações, mas você não verá as deles.",
"navigation_bar.about": "Sobre",
"navigation_bar.administration": "Administração",
"navigation_bar.advanced_interface": "Abrir na interface web avançada",
"navigation_bar.blocks": "Utilizadores bloqueados",
"navigation_bar.bookmarks": "Marcadores",
@ -476,6 +486,7 @@
"navigation_bar.follows_and_followers": "Seguindo e seguidores",
"navigation_bar.lists": "Listas",
"navigation_bar.logout": "Sair",
"navigation_bar.moderation": "Moderação",
"navigation_bar.mutes": "Utilizadores silenciados",
"navigation_bar.opened_in_classic_interface": "Por norma, publicações, contas, e outras páginas específicas são abertas na interface web clássica.",
"navigation_bar.personal": "Pessoal",
@ -491,9 +502,13 @@
"notification.admin.report_statuses": "{name} denunicou {target} por {category}",
"notification.admin.report_statuses_other": "{name} denunciou {target}",
"notification.admin.sign_up": "{name} inscreveu-se",
"notification.admin.sign_up.name_and_others": "{name} e {count, plural, one {# outro} other {# outros}} inscreveram-se",
"notification.favourite": "{name} assinalou a sua publicação como favorita",
"notification.favourite.name_and_others_with_link": "{name} e <a>{count, plural, one {# outro} other {# outros}}</a> assinalou a sua publicação como favorita",
"notification.follow": "{name} começou a seguir-te",
"notification.follow.name_and_others": "{name} e {count, plural, one {# outro} other {# outros}} começaram a segui-lo",
"notification.follow_request": "{name} pediu para segui-lo",
"notification.follow_request.name_and_others": "{name} e {count, plural, one {# outro} other {# outros}} pediram para segui-lo",
"notification.label.mention": "Menção",
"notification.label.private_mention": "Menção privada",
"notification.label.private_reply": "Resposta privada",
@ -511,6 +526,7 @@
"notification.own_poll": "A sua sondagem terminou",
"notification.poll": "Terminou uma sondagem em que votou",
"notification.reblog": "{name} reforçou a tua publicação",
"notification.reblog.name_and_others_with_link": "{name} e <a>{count, plural, one {# outro} other {# outros}}</a> reforçaram a sua publicação",
"notification.relationships_severance_event": "Perdeu as ligações com {name}",
"notification.relationships_severance_event.account_suspension": "Um administrador de {from} suspendeu {target}, o que significa que já não pode receber atualizações dele ou interagir com ele.",
"notification.relationships_severance_event.domain_block": "Um administrador de {from} bloqueou {target}, incluindo {followersCount} dos seus seguidores e {followingCount, plural, one {# conta} other {# contas}} que segue.",
@ -519,13 +535,24 @@
"notification.status": "{name} acabou de publicar",
"notification.update": "{name} editou uma publicação",
"notification_requests.accept": "Aceitar",
"notification_requests.accept_multiple": "{count, plural, one {Aceitar # pedidos…} other {Aceitar # pedidos…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceitar pedido} other {Aceitar pedidos}}",
"notification_requests.confirm_accept_multiple.message": "Está prestes a aceitar {count, plural, one {um pedido de notificação} other {# pedidos de notificação}}. Tem a certeza de que pretende continuar?",
"notification_requests.confirm_accept_multiple.title": "Aceitar pedidos de notificação?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Rejeitar pedido} other {Rejeitar pedidos}}",
"notification_requests.confirm_dismiss_multiple.message": "Está prestes a rejeitar {count, plural, one {um pedido de notificação} other {# pedidos de notificação}}. Não será fácil voltar a {count, plural, one {aceder-lhe} other {aceder-lhes}}. Tem a certeza de que pretende continuar?",
"notification_requests.confirm_dismiss_multiple.title": "Rejeitar pedidos de notificação?",
"notification_requests.dismiss": "Descartar",
"notification_requests.dismiss_multiple": "{count, plural, one {Rejeitar # pedido…} other {Rejeitar # pedidos…}}",
"notification_requests.edit_selection": "Editar",
"notification_requests.exit_selection": "Concluído",
"notification_requests.explainer_for_limited_account": "As notificações desta conta foram filtradas porque a conta foi limitada por um moderador.",
"notification_requests.explainer_for_limited_remote_account": "As notificações desta conta foram filtradas porque a conta ou o seu servidor foram limitados por um moderador.",
"notification_requests.maximize": "Maximizar",
"notification_requests.minimize_banner": "Minimizar o cabeçalho das notificações filtradas",
"notification_requests.notifications_from": "Notificações de {name}",
"notification_requests.title": "Notificações filtradas",
"notification_requests.view": "Ver notificações",
"notifications.clear": "Limpar notificações",
"notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
"notifications.clear_title": "Limpar notificações?",
@ -562,6 +589,12 @@
"notifications.permission_denied": "Notificações no ambiente de trabalho não estão disponíveis porque a permissão, solicitada pelo navegador, foi recusada anteriormente",
"notifications.permission_denied_alert": "Notificações no ambiente de trabalho não podem ser ativadas, pois a permissão do navegador foi recusada anteriormente",
"notifications.permission_required": "Notificações no ambiente de trabalho não estão disponíveis porque a permissão necessária não foi concedida.",
"notifications.policy.accept": "Aceitar",
"notifications.policy.accept_hint": "Mostrar nas notificações",
"notifications.policy.drop": "Ignorar",
"notifications.policy.drop_hint": "Enviar para o vazio, para nunca mais ser visto",
"notifications.policy.filter": "Filtrar",
"notifications.policy.filter_hint": "Enviar para a caixa de notificações filtradas",
"notifications.policy.filter_limited_accounts_hint": "Limitado pelos moderadores do servidor",
"notifications.policy.filter_limited_accounts_title": "Contas moderadas",
"notifications.policy.filter_new_accounts.hint": "Criada nos últimos {days, plural, one {um dia} other {# dias}}",
@ -572,6 +605,7 @@
"notifications.policy.filter_not_following_title": "Pessoas que você não segue",
"notifications.policy.filter_private_mentions_hint": "Filtrado, a menos que seja em resposta à sua própria menção ou se você seguir o remetente",
"notifications.policy.filter_private_mentions_title": "Menções privadas não solicitadas",
"notifications.policy.title": "Gerir notificações de…",
"notifications_permission_banner.enable": "Ativar notificações no ambiente de trabalho",
"notifications_permission_banner.how_to_control": "Para receber notificações quando o Mastodon não estiver aberto, ative as notificações no ambiente de trabalho. Depois da sua ativação, pode controlar precisamente quais tipos de interações geram notificações, através do botão {icon} acima.",
"notifications_permission_banner.title": "Nunca perca nada",
@ -746,6 +780,7 @@
"status.bookmark": "Guardar nos marcadores",
"status.cancel_reblog_private": "Deixar de reforçar",
"status.cannot_reblog": "Não é possível partilhar esta publicação",
"status.continued_thread": "Continuação da conserva",
"status.copy": "Copiar hiperligação para a publicação",
"status.delete": "Eliminar",
"status.detailed_status": "Vista pormenorizada da conversa",
@ -754,6 +789,7 @@
"status.edit": "Editar",
"status.edited": "Última edição em {date}",
"status.edited_x_times": "Editado {count, plural,one {{count} vez} other {{count} vezes}}",
"status.embed": "Obter código de incorporação",
"status.favourite": "Assinalar como favorito",
"status.favourites": "{count, plural, one {favorito} other {favoritos}}",
"status.filter": "Filtrar esta publicação",
@ -778,6 +814,7 @@
"status.reblogs.empty": "Ainda ninguém reforçou esta publicação. Quando alguém o fizer, ele irá aparecer aqui.",
"status.redraft": "Apagar & reescrever",
"status.remove_bookmark": "Retirar dos marcadores",
"status.replied_in_thread": "Responder na conversa",
"status.replied_to": "Respondeu a {name}",
"status.reply": "Responder",
"status.replyAll": "Responder à conversa",

View file

@ -457,6 +457,7 @@
"lists.subheading": "รายการของคุณ",
"load_pending": "{count, plural, other {# รายการใหม่}}",
"loading_indicator.label": "กำลังโหลด…",
"media_gallery.hide": "ซ่อน",
"moved_to_account_banner.text": "มีการปิดใช้งานบัญชีของคุณ {disabledAccount} ในปัจจุบันเนื่องจากคุณได้ย้ายไปยัง {movedToAccount}",
"mute_modal.hide_from_notifications": "ซ่อนจากการแจ้งเตือน",
"mute_modal.hide_options": "ซ่อนตัวเลือก",
@ -779,6 +780,7 @@
"status.bookmark": "เพิ่มที่คั่นหน้า",
"status.cancel_reblog_private": "เลิกดัน",
"status.cannot_reblog": "ไม่สามารถดันโพสต์นี้",
"status.continued_thread": "กระทู้ต่อเนื่อง",
"status.copy": "คัดลอกลิงก์ไปยังโพสต์",
"status.delete": "ลบ",
"status.detailed_status": "มุมมองการสนทนาโดยละเอียด",
@ -787,6 +789,7 @@
"status.edit": "แก้ไข",
"status.edited": "แก้ไขล่าสุดเมื่อ {date}",
"status.edited_x_times": "แก้ไข {count, plural, other {{count} ครั้ง}}",
"status.embed": "รับโค้ดฝังตัว",
"status.favourite": "ชื่นชอบ",
"status.favourites": "{count, plural, other {รายการโปรด}}",
"status.filter": "กรองโพสต์นี้",
@ -811,6 +814,7 @@
"status.reblogs.empty": "ยังไม่มีใครดันโพสต์นี้ เมื่อใครสักคนดัน เขาจะปรากฏที่นี่",
"status.redraft": "ลบแล้วร่างใหม่",
"status.remove_bookmark": "เอาที่คั่นหน้าออก",
"status.replied_in_thread": "ตอบกลับในกระทู้",
"status.replied_to": "ตอบกลับ {name}",
"status.reply": "ตอบกลับ",
"status.replyAll": "ตอบกลับกระทู้",

View file

@ -457,6 +457,7 @@
"lists.subheading": "Danh sách của bạn",
"load_pending": "{count, plural, one {# tút mới} other {# tút mới}}",
"loading_indicator.label": "Đang tải…",
"media_gallery.hide": "Ẩn",
"moved_to_account_banner.text": "Tài khoản {disabledAccount} của bạn hiện không khả dụng vì bạn đã chuyển sang {movedToAccount}.",
"mute_modal.hide_from_notifications": "Ẩn thông báo",
"mute_modal.hide_options": "Tùy chọn ẩn",
@ -779,6 +780,7 @@
"status.bookmark": "Lưu",
"status.cancel_reblog_private": "Hủy đăng lại",
"status.cannot_reblog": "Không thể đăng lại tút này",
"status.continued_thread": "Tiếp tục trong chủ đề",
"status.copy": "Sao chép URL",
"status.delete": "Xóa",
"status.detailed_status": "Xem chi tiết thêm",
@ -787,6 +789,7 @@
"status.edit": "Sửa",
"status.edited": "Sửa lần cuối {date}",
"status.edited_x_times": "Đã sửa {count, plural, other {{count} lần}}",
"status.embed": "Lấy mã nhúng",
"status.favourite": "Thích",
"status.favourites": "{count, plural, other {lượt thích}}",
"status.filter": "Lọc tút này",
@ -811,6 +814,7 @@
"status.reblogs.empty": "Tút này chưa có ai đăng lại. Nếu có, nó sẽ hiển thị ở đây.",
"status.redraft": "Xóa và viết lại",
"status.remove_bookmark": "Bỏ lưu",
"status.replied_in_thread": "Trả lời trong chủ đề",
"status.replied_to": "Trả lời {name}",
"status.reply": "Trả lời",
"status.replyAll": "Trả lời",

View file

@ -0,0 +1,19 @@
import type { ApiNotificationRequestJSON } from 'mastodon/api_types/notifications';
export interface NotificationRequest
extends Omit<ApiNotificationRequestJSON, 'account' | 'notifications_count'> {
account_id: string;
notifications_count: number;
}
export function createNotificationRequestFromJSON(
requestJSON: ApiNotificationRequestJSON,
): NotificationRequest {
const { account, notifications_count, ...request } = requestJSON;
return {
account_id: account.id,
notifications_count: +notifications_count,
...request,
};
}

View file

@ -1,114 +0,0 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { blockAccountSuccess, muteAccountSuccess } from 'mastodon/actions/accounts';
import {
NOTIFICATION_REQUESTS_EXPAND_REQUEST,
NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
NOTIFICATION_REQUESTS_EXPAND_FAIL,
NOTIFICATION_REQUESTS_FETCH_REQUEST,
NOTIFICATION_REQUESTS_FETCH_SUCCESS,
NOTIFICATION_REQUESTS_FETCH_FAIL,
NOTIFICATION_REQUEST_FETCH_REQUEST,
NOTIFICATION_REQUEST_FETCH_SUCCESS,
NOTIFICATION_REQUEST_FETCH_FAIL,
NOTIFICATION_REQUEST_ACCEPT_REQUEST,
NOTIFICATION_REQUEST_DISMISS_REQUEST,
NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
NOTIFICATION_REQUESTS_DISMISS_REQUEST,
NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
} from 'mastodon/actions/notifications';
import { notificationToMap } from './notifications';
const initialState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
next: null,
current: ImmutableMap({
isLoading: false,
item: null,
removed: false,
notifications: ImmutableMap({
items: ImmutableList(),
isLoading: false,
next: null,
}),
}),
});
const normalizeRequest = request => fromJS({
...request,
account: request.account.id,
});
const removeRequest = (state, id) => {
if (state.getIn(['current', 'item', 'id']) === id) {
state = state.setIn(['current', 'removed'], true);
}
return state.update('items', list => list.filterNot(item => item.get('id') === id));
};
const removeRequestByAccount = (state, account_id) => {
if (state.getIn(['current', 'item', 'account']) === account_id) {
state = state.setIn(['current', 'removed'], true);
}
return state.update('items', list => list.filterNot(item => item.get('account') === account_id));
};
export const notificationRequestsReducer = (state = initialState, action) => {
switch(action.type) {
case NOTIFICATION_REQUESTS_FETCH_SUCCESS:
return state.withMutations(map => {
map.update('items', list => ImmutableList(action.requests.map(normalizeRequest)).concat(list));
map.set('isLoading', false);
map.update('next', next => next ?? action.next);
});
case NOTIFICATION_REQUESTS_EXPAND_SUCCESS:
return state.withMutations(map => {
map.update('items', list => list.concat(ImmutableList(action.requests.map(normalizeRequest))));
map.set('isLoading', false);
map.set('next', action.next);
});
case NOTIFICATION_REQUESTS_EXPAND_REQUEST:
case NOTIFICATION_REQUESTS_FETCH_REQUEST:
return state.set('isLoading', true);
case NOTIFICATION_REQUESTS_EXPAND_FAIL:
case NOTIFICATION_REQUESTS_FETCH_FAIL:
return state.set('isLoading', false);
case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
case NOTIFICATION_REQUEST_DISMISS_REQUEST:
return removeRequest(state, action.id);
case NOTIFICATION_REQUESTS_ACCEPT_REQUEST:
case NOTIFICATION_REQUESTS_DISMISS_REQUEST:
return action.ids.reduce((state, id) => removeRequest(state, id), state);
case blockAccountSuccess.type:
return removeRequestByAccount(state, action.payload.relationship.id);
case muteAccountSuccess.type:
return action.payload.relationship.muting_notifications ? removeRequestByAccount(state, action.payload.relationship.id) : state;
case NOTIFICATION_REQUEST_FETCH_REQUEST:
return state.set('current', initialState.get('current').set('isLoading', true));
case NOTIFICATION_REQUEST_FETCH_SUCCESS:
return state.update('current', map => map.set('isLoading', false).set('item', normalizeRequest(action.request)));
case NOTIFICATION_REQUEST_FETCH_FAIL:
return state.update('current', map => map.set('isLoading', false));
case NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST:
case NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST:
return state.setIn(['current', 'notifications', 'isLoading'], true);
case NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS:
return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => ImmutableList(action.notifications.map(notificationToMap)).concat(list)).update('next', next => next ?? action.next));
case NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS:
return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => list.concat(ImmutableList(action.notifications.map(notificationToMap)))).set('next', action.next));
case NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL:
case NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL:
return state.setIn(['current', 'notifications', 'isLoading'], false);
default:
return state;
}
};

View file

@ -0,0 +1,182 @@
import { createReducer, isAnyOf } from '@reduxjs/toolkit';
import {
blockAccountSuccess,
muteAccountSuccess,
} from 'mastodon/actions/accounts';
import {
fetchNotificationRequests,
expandNotificationRequests,
fetchNotificationRequest,
fetchNotificationsForRequest,
expandNotificationsForRequest,
acceptNotificationRequest,
dismissNotificationRequest,
acceptNotificationRequests,
dismissNotificationRequests,
} from 'mastodon/actions/notification_requests';
import type { NotificationRequest } from 'mastodon/models/notification_request';
import { createNotificationRequestFromJSON } from 'mastodon/models/notification_request';
import { notificationToMap } from './notifications';
interface NotificationsListState {
items: unknown[]; // TODO
isLoading: boolean;
next: string | null;
}
interface CurrentNotificationRequestState {
item: NotificationRequest | null;
isLoading: boolean;
removed: boolean;
notifications: NotificationsListState;
}
interface NotificationRequestsState {
items: NotificationRequest[];
isLoading: boolean;
next: string | null;
current: CurrentNotificationRequestState;
}
const initialState: NotificationRequestsState = {
items: [],
isLoading: false,
next: null,
current: {
item: null,
isLoading: false,
removed: false,
notifications: {
isLoading: false,
items: [],
next: null,
},
},
};
const removeRequest = (state: NotificationRequestsState, id: string) => {
if (state.current.item?.id === id) {
state.current.removed = true;
}
state.items = state.items.filter((item) => item.id !== id);
};
const removeRequestByAccount = (
state: NotificationRequestsState,
account_id: string,
) => {
if (state.current.item?.account_id === account_id) {
state.current.removed = true;
}
state.items = state.items.filter((item) => item.account_id !== account_id);
};
export const notificationRequestsReducer =
createReducer<NotificationRequestsState>(initialState, (builder) => {
builder
.addCase(fetchNotificationRequests.fulfilled, (state, action) => {
state.items = action.payload.requests
.map(createNotificationRequestFromJSON)
.concat(state.items);
state.isLoading = false;
state.next ??= action.payload.next ?? null;
})
.addCase(expandNotificationRequests.fulfilled, (state, action) => {
state.items = state.items.concat(
action.payload.requests.map(createNotificationRequestFromJSON),
);
state.isLoading = false;
state.next = action.payload.next ?? null;
})
.addCase(blockAccountSuccess, (state, action) => {
removeRequestByAccount(state, action.payload.relationship.id);
})
.addCase(muteAccountSuccess, (state, action) => {
if (action.payload.relationship.muting_notifications)
removeRequestByAccount(state, action.payload.relationship.id);
})
.addCase(fetchNotificationRequest.pending, (state) => {
state.current = { ...initialState.current, isLoading: true };
})
.addCase(fetchNotificationRequest.rejected, (state) => {
state.current.isLoading = false;
})
.addCase(fetchNotificationRequest.fulfilled, (state, action) => {
state.current.isLoading = false;
state.current.item = createNotificationRequestFromJSON(action.payload);
})
.addCase(fetchNotificationsForRequest.fulfilled, (state, action) => {
state.current.notifications.isLoading = false;
state.current.notifications.items.unshift(
...action.payload.notifications.map(notificationToMap),
);
state.current.notifications.next ??= action.payload.next ?? null;
})
.addCase(expandNotificationsForRequest.fulfilled, (state, action) => {
state.current.notifications.isLoading = false;
state.current.notifications.items.push(
...action.payload.notifications.map(notificationToMap),
);
state.current.notifications.next = action.payload.next ?? null;
})
.addMatcher(
isAnyOf(
fetchNotificationRequests.pending,
expandNotificationRequests.pending,
),
(state) => {
state.isLoading = true;
},
)
.addMatcher(
isAnyOf(
fetchNotificationRequests.rejected,
expandNotificationRequests.rejected,
),
(state) => {
state.isLoading = false;
},
)
.addMatcher(
isAnyOf(
acceptNotificationRequest.pending,
dismissNotificationRequest.pending,
),
(state, action) => {
removeRequest(state, action.meta.arg.id);
},
)
.addMatcher(
isAnyOf(
acceptNotificationRequests.pending,
dismissNotificationRequests.pending,
),
(state, action) => {
action.meta.arg.ids.forEach((id) => {
removeRequest(state, id);
});
},
)
.addMatcher(
isAnyOf(
fetchNotificationsForRequest.pending,
expandNotificationsForRequest.pending,
),
(state) => {
state.current.notifications.isLoading = true;
},
)
.addMatcher(
isAnyOf(
fetchNotificationsForRequest.rejected,
expandNotificationsForRequest.rejected,
),
(state) => {
state.current.notifications.isLoading = false;
},
);
});

View file

@ -33,8 +33,12 @@ interface AppThunkConfig {
}
type AppThunkApi = Pick<GetThunkAPI<AppThunkConfig>, 'getState' | 'dispatch'>;
interface AppThunkOptions {
interface AppThunkOptions<Arg> {
useLoadingBar?: boolean;
condition?: (
arg: Arg,
{ getState }: { getState: AppThunkApi['getState'] },
) => boolean;
}
const createBaseAsyncThunk = createAsyncThunk.withTypes<AppThunkConfig>();
@ -42,7 +46,7 @@ const createBaseAsyncThunk = createAsyncThunk.withTypes<AppThunkConfig>();
export function createThunk<Arg = void, Returned = void>(
name: string,
creator: (arg: Arg, api: AppThunkApi) => Returned | Promise<Returned>,
options: AppThunkOptions = {},
options: AppThunkOptions<Arg> = {},
) {
return createBaseAsyncThunk(
name,
@ -70,6 +74,7 @@ export function createThunk<Arg = void, Returned = void>(
if (options.useLoadingBar) return { useLoadingBar: true };
return {};
},
condition: options.condition,
},
);
}
@ -96,7 +101,7 @@ type ArgsType = Record<string, unknown> | undefined;
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string,
loadData: (args: Args) => Promise<LoadDataResult>,
thunkOptions?: AppThunkOptions,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, LoadDataResult>>;
// Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty
@ -104,17 +109,19 @@ export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string,
loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?:
| AppThunkOptions
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, DiscardLoadData>,
thunkOptions?: AppThunkOptions,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, void>>;
// Overload when the `onData` method returns nothing, then the mayload is the `onData` result
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string,
loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: AppThunkOptions | OnData<Args, LoadDataResult, void>,
thunkOptions?: AppThunkOptions,
onDataOrThunkOptions?:
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, void>,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, LoadDataResult>>;
// Overload when there is an `onData` method returning something
@ -126,9 +133,9 @@ export function createDataLoadingThunk<
name: string,
loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?:
| AppThunkOptions
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, Returned>,
thunkOptions?: AppThunkOptions,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, Returned>>;
/**
@ -154,6 +161,7 @@ export function createDataLoadingThunk<
* @param maybeThunkOptions
* Additional Mastodon specific options for the thunk. Currently supports:
* - `useLoadingBar` to display a loading bar while this action is pending. Defaults to true.
* - `condition` is passed to `createAsyncThunk` (https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution)
* @returns The created thunk
*/
export function createDataLoadingThunk<
@ -164,12 +172,12 @@ export function createDataLoadingThunk<
name: string,
loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?:
| AppThunkOptions
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, Returned>,
maybeThunkOptions?: AppThunkOptions,
maybeThunkOptions?: AppThunkOptions<Args>,
) {
let onData: OnData<Args, LoadDataResult, Returned> | undefined;
let thunkOptions: AppThunkOptions | undefined;
let thunkOptions: AppThunkOptions<Args> | undefined;
if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions;
else if (typeof onDataOrThunkOptions === 'object')
@ -203,6 +211,9 @@ export function createDataLoadingThunk<
return undefined as Returned;
else return result;
},
{ useLoadingBar: thunkOptions?.useLoadingBar ?? true },
{
useLoadingBar: thunkOptions?.useLoadingBar ?? true,
condition: thunkOptions?.condition,
},
);
}

View file

@ -10234,6 +10234,7 @@ noscript {
scroll-padding: 16px;
scroll-behavior: smooth;
overflow-x: scroll;
scrollbar-width: none;
&__card {
background: var(--background-color);

View file

@ -26,6 +26,7 @@ class CustomEmoji < ApplicationRecord
LOCAL_LIMIT = (ENV['MAX_EMOJI_SIZE'] || 256.kilobytes).to_i
LIMIT = [LOCAL_LIMIT, (ENV['MAX_REMOTE_EMOJI_SIZE'] || 256.kilobytes).to_i].max
MINIMUM_SHORTCODE_SIZE = 2
SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
@ -47,7 +48,7 @@ class CustomEmoji < ApplicationRecord
validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true
validates_attachment_size :image, less_than: LIMIT, unless: :local?
validates_attachment_size :image, less_than: LOCAL_LIMIT, if: :local?
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: 2 }
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE }
scope :local, -> { where(domain: nil) }
scope :remote, -> { where.not(domain: nil) }

View file

@ -39,7 +39,7 @@ class PreviewCard < ApplicationRecord
include Attachmentable
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
LIMIT = 2.megabytes
LIMIT = Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes
BLURHASH_OPTIONS = {
x_comp: 4,

View file

@ -109,9 +109,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
end
def api_versions
{
mastodon: 1,
}
Mastodon::Version.api_versions
end
private

View file

@ -0,0 +1,4 @@
# frozen_string_literal: true
# 0.5s is a fairly high timeout, but that should account for slow servers under load
Regexp.timeout = 0.5 if Regexp.respond_to?(:timeout=)

View file

@ -15,6 +15,12 @@ et:
user/invite_request:
text: Põhjus
errors:
attributes:
domain:
invalid: pole kehtiv domeeninimi
messages:
invalid_domain_on_line: "%{value} ei ole kehtiv domeeninimi"
too_many_lines: on üle limiidi %{limit} rida
models:
account:
attributes:

View file

@ -18,6 +18,9 @@ fr-CA:
attributes:
domain:
invalid: n'est pas un nom de domaine valide
messages:
invalid_domain_on_line: "%{value} n'est pas un nom de domaine valide"
too_many_lines: dépasse la limite de %{limit} lignes
models:
account:
attributes:

View file

@ -18,6 +18,9 @@ fr:
attributes:
domain:
invalid: n'est pas un nom de domaine valide
messages:
invalid_domain_on_line: "%{value} n'est pas un nom de domaine valide"
too_many_lines: dépasse la limite de %{limit} lignes
models:
account:
attributes:

View file

@ -15,6 +15,12 @@ ga:
user/invite_request:
text: Fáth
errors:
attributes:
domain:
invalid: nach ainm fearainn bailí é
messages:
invalid_domain_on_line: Ní ainm fearainn bailí é %{value}
too_many_lines: thar an teorainn de %{limit} línte
models:
account:
attributes:

View file

@ -15,6 +15,12 @@ gd:
user/invite_request:
text: Adhbhar
errors:
attributes:
domain:
invalid: " chan eil seo na ainm àrainne dligheach"
messages:
invalid_domain_on_line: Chan eil %{value} na ainm àrainne dligheach
too_many_lines: " tha seo thar crìoch de %{limit} nan loidhnichean"
models:
account:
attributes:

View file

@ -11,15 +11,21 @@ ro:
locale: Localizare
password: Parolă
user/account:
username: Nume utilizator
username: Nume de utilizator
user/invite_request:
text: Motiv
errors:
attributes:
domain:
invalid: nu este un nume de domeniu valid
messages:
invalid_domain_on_line: "%{value} nu este un nume de domeniu valid"
too_many_lines: este peste limita de %{limit} linii
models:
account:
attributes:
username:
invalid: doar litere, numere și sublinieri
invalid: trebuie să conțină numai litere, cifre și bară jos (_)
reserved: este rezervat
admin/webhook:
attributes:
@ -56,4 +62,4 @@ ro:
webhook:
attributes:
events:
invalid_permissions: nu poate include evenimente la care nu aveți drepturi
invalid_permissions: nu poate include evenimente la care nu aveți dreptul

View file

@ -15,6 +15,12 @@ th:
user/invite_request:
text: เหตุผล
errors:
attributes:
domain:
invalid: ไม่ใช่ชื่อโดเมนที่ถูกต้อง
messages:
invalid_domain_on_line: "%{value} ไม่ใช่ชื่อโดเมนที่ถูกต้อง"
too_many_lines: เกินขีดจำกัด %{limit} บรรทัด
models:
account:
attributes:

View file

@ -15,6 +15,12 @@ vi:
user/invite_request:
text: Lý do
errors:
attributes:
domain:
invalid: không phải là một tên miền hợp lệ
messages:
invalid_domain_on_line: "%{value} không phải là một tên miền hợp lệ"
too_many_lines: vượt quá giới hạn %{limit} dòng
models:
account:
attributes:

View file

@ -14,9 +14,9 @@ lv:
not_found_in_database: Nederīga %{authentication_keys} vai parole.
omniauth_user_creation_failure: Kļūda šīs identitātes konta izveidošanā.
pending: Tavs konts joprojām tiek pārskatīts.
timeout: Tava sesija ir beigusies. Lūdzu, pieraksties vēlreiz, lai turpinātu.
unauthenticated: Lai turpinātu, tev ir jāpierakstās vai jāreģistrējas.
unconfirmed: Lai turpinātu, tev ir jāapstiprina savu e-pasta adresi.
timeout: Sesijair beigusies. Lūgums vēlreiz pieteikties, lai turpinātu.
unauthenticated: Lai turpinātu, jāpiesakās vai jāreģistrējas.
unconfirmed: Lai turpinātu, jāapliecina sava e-pasta adrese.
mailer:
confirmation_instructions:
action: Apstiprini savu e-pasta adresi
@ -108,7 +108,7 @@ lv:
unlocks:
send_instructions: Pēc dažām minūtēm tu saņemsi e-pastu ar norādījumiem, kā atbloķēt savu kontu. Lūdzu, pārbaudi spama mapi, ja neesi saņēmis šo e-pastu.
send_paranoid_instructions: Ja tavs konts eksistē, dažu minūšu laikā tu saņemsi e-pastu ar norādījumiem, kā to atbloķēt. Lūdzu, pārbaudi spama mapi, ja neesi saņēmis šo e-pastu.
unlocked: Tavs konts ir veiksmīgi atbloķēts. Lūdzu, pieraksties, lai turpinātu.
unlocked: Konts tika veiksmīgi atbloķēts. Lūgums pieteikties, lai turpinātu.
errors:
messages:
already_confirmed: jau tika apstiprināts, lūdzu, mēģini pierakstīties

View file

@ -2,58 +2,58 @@
ro:
devise:
confirmations:
confirmed: Adresa ta de e-mail a fost confirmată cu succes.
send_instructions: Vei primi un e-mail cu instrucțiuni despre cum să confirmi adresa ta de e-mail în câteva minute. Te rugăm să verifici dosarul spam dacă nu ai primit acest e-mail.
send_paranoid_instructions: Dacă adresa ta de e-mail există în baza noastră de date, în câteva minute vei primi un e-mail cu instrucțiuni pentru confirmarea adresei tale de e-mail. Te rugăm să verifici dosarul spam dacă nu ai primit acest e-mail.
confirmed: Adresa dvs. de e-mail a fost confirmată cu succes.
send_instructions: Veți primi un e-mail în câteva minute cu instrucțiuni despre cum să vă confirmați adresa de e-mail. Vă rugăm să verificați dosarul spam dacă nu ați primit acest e-mail.
send_paranoid_instructions: Dacă adresa dvs. de e-mail există în baza noastră de date, veți primi în câteva minute un e-mail cu instrucțiuni pentru confirmarea adresei de e-mail. Vă rugăm să verificați dosarul spam dacă nu ați primit acest e-mail.
failure:
already_authenticated: Ești deja conectat.
inactive: Contul tău nu este încă activat.
already_authenticated: Sunteți deja conectat.
inactive: Contul dvs. nu este încă activat.
invalid: "%{authentication_keys} sau parolă greșită."
last_attempt: Mai ai încă o încercare înainte ca contul tău să fie blocat.
locked: Contul tău este blocat.
last_attempt: Mai aveți o încercare înainte ca contul dvs. să fie blocat.
locked: Contul dvs. este blocat.
not_found_in_database: "%{authentication_keys} sau parolă greșită."
omniauth_user_creation_failure: Eroare la crearea unui cont pentru această identitate.
pending: Contul tău este încă în curs de revizuire.
timeout: Sesiunea ta a expirat. Te rugăm să te conectezi din nou pentru a continua.
unauthenticated: Trebuie să te conectezi sau să te înregistrezi înainte de a continua.
unconfirmed: Trebuie să confirmi adresa ta de e-mail înainte de a continua.
pending: Contul dvs. este încă în curs de revizuire.
timeout: Sesiunea dvs. a expirat. Vă rugăm să vă conectați din nou pentru a continua.
unauthenticated: Trebuie să vă conectați sau să vă înregistrați înainte de a continua.
unconfirmed: Trebuie să confirmi adresa de e-mail înainte de a continua.
mailer:
confirmation_instructions:
action: Verifică adresa de e-mail
action: Verificare adresă de e-mail
action_with_app: Confirmați și reveniți la %{app}
explanation: Ai creat un cont pe %{host} cu această adresă de e-mail. Ești la un clic distanță de a-l activa. Dacă nu ai fost tu, ignoră acest e-mail.
explanation_when_pending: Ai solicitat o invitație către %{host} cu această adresă de e-mail. Odată ce îți confirmi adresa de e-mail, îți vom revizui cererea. Te poți autentifica pentru a-ți schimba detaliile sau pentru a-ți șterge contul, dar nu poți accesa majoritatea funcțiilor până când contul tău nu este aprobat. Dacă cererea ta este respinsă, datele tale vor fi șterse, astfel încât nu va fi necesară nicio altă acțiune din partea ta. Dacă nu ai fost tu, ignoră acest e-mail.
explanation: Ați creat un cont pe %{host} cu această adresă de e-mail. Sunteți la un clic distanță de a-l activa. Dacă nu ați fost dvs., vă rugăm să ignorați acest e-mail.
explanation_when_pending: Ați aplicat pentru o invitație pentru %{host} cu această adresă de e-mail. Odată ce vă confirmați adresa de e-mail, vă vom examina cererea. Vă puteți autentifica pentru a vă schimba detaliile sau pentru a vă șterge contul, dar nu puteți accesa majoritatea funcțiilor până când contul dvs. nu este aprobat. Dacă cererea dvs. este respinsă, datele dvs. vor fi șterse, astfel încât nu va fi necesară nicio acțiune suplimentară din partea dvs. Dacă nu ați fost dvs., vă rugăm să ignorați acest e-mail.
extra_html: Te rugăm să verifici și <a href="%{terms_path}">regulile serverului</a> și <a href="%{policy_path}">termenii noștri de serviciu</a>.
subject: 'Mastodon: Instrucțiuni de confirmare pentru %{instance}'
title: Verifică adresa de e-mail
title: Verificați adresa de e-mail
email_changed:
explanation: 'Adresa de e-mail pentru contul tău este schimbată la:'
extra: Dacă nu v-ați schimbat adresa de e-mail, probabil că cineva a obținut acces la contul dvs. Te rugăm să îți schimbi parola imediat sau să contactezi administratorul serverului dacă nu ai acces la contul tău.
subject: 'Mastodon: E-mail schimbat'
title: Noua adresa de e-mail
explanation: 'Adresa de e-mail a contului dvs. este schimbată în:'
extra: Dacă nu v-ați schimbat adresa de e-mail, probabil că cineva a obținut acces la contul dvs. Vă rugăm să vă schimbați parola imediat sau să contactați administratorul serverului dacă nu aveți acces la contul dvs.
subject: 'Mastodon: Adresă de e-mail schimbată'
title: Adresă de e-mail nouă
password_change:
explanation: Parola contului tău a fost schimbată.
extra: Dacă nu v-ați schimbat parola, este posibil ca cineva să fi obținut acces la contul dvs. Te rugăm să îți schimbi parola imediat sau să contactezi administratorul serverului dacă nu ai acces la contul tău.
explanation: Parola pentru contul dvs. a fost schimbată.
extra: Dacă nu v-ați schimbat parola, probabil că cineva a obținut acces la contul dvs. Vă rugăm să vă schimbați parola imediat sau să contactați administratorul serverului dacă nu aveți acces la contul dvs.
subject: 'Mastodon: Parolă schimbată'
title: Parolă schimbată
reconfirmation_instructions:
explanation: Confirmă noua adresă pentru a schimba adresa de e-mail.
explanation: Confirmați noua adresă pentru a vă schimba adresa de e-mail.
extra: Dacă această modificare nu a fost inițiată de dvs., vă rugăm să ignorați acest e-mail. Adresa de e-mail pentru contul Mastodon nu se va schimba până când nu accesați link-ul de mai sus.
subject: 'Mastodon: Confirmați e-mailul pentru %{instance}'
title: Verifică adresa de e-mail
title: Verificați adresa de e-mail
reset_password_instructions:
action: Schimbă parola
action: Schimbați parola
explanation: Ați solicitat o nouă parolă pentru contul dvs.
extra: Dacă nu ați solicitat acest lucru, ignorați acest e-mail. Parola dvs. nu se va schimba până când nu veți accesa link-ul de mai sus și nu veți crea unul nou.
extra: Dacă nu ați solicitat acest lucru, vă rugăm să ignorați acest e-mail. Parola dvs. nu se va schimba până când nu veți accesa link-ul de mai sus și nu veți crea unul nou.
subject: 'Mastodon: Instrucțiuni pentru resetarea parolei'
title: Resetare parolă
two_factor_disabled:
explanation: Conectarea este acum posibilă folosind doar adresa de e-mail și parola.
subject: 'Mastodon: Autentificare cu doi factori dezactivată'
subject: 'Mastodon: Autentificarea cu doi factori dezactivată'
subtitle: Autentificarea cu doi factori pentru contul dvs. a fost dezactivată.
title: A2F dezactivată
two_factor_enabled:
explanation: Pentru autentificare va fi necesar un token generat de aplicația TOTP asociată.
explanation: Pentru conectare va fi necesar un token generat de aplicația TOTP asociată.
subject: 'Mastodon: Autentificare în doi pași activată'
subtitle: Autentificarea cu doi factori a fost activată pentru contul dvs.
title: A2F activată
@ -61,18 +61,18 @@ ro:
explanation: Codurile de recuperare anterioare au fost invalidate și s-au generat altele noi.
subject: 'Mastodon: Coduri de recuperare în doi pași regenerate'
subtitle: Codurile de recuperare anterioare au fost invalidate și s-au generat altele noi.
title: Codurile de recuperare în doi pași au fost modificate
title: Codurile de recuperare în doi pași modificate
unlock_instructions:
subject: 'Mastodon: Instrucțiuni de deblocare'
webauthn_credential:
added:
explanation: Următoarea cheie de securitate a fost adăugată în contul tău
explanation: Următoarea cheie de securitate a fost adăugată în contul dvs.
subject: 'Mastodon: Noua cheie de securitate'
title: A fost adăugată o nouă cheie de securitate
deleted:
explanation: Următoarea cheie de securitate a fost ștearsă din contul tău
explanation: Următoarea cheie de securitate a fost ștearsă din contul dvs.
subject: 'Mastodon: Cheie de securitate ștearsă'
title: Una dintre cheile tale de securitate a fost ștearsă
title: Una dintre cheile dvs. de securitate a fost ștearsă
webauthn_disabled:
explanation: Autentificarea cu chei de securitate a fost dezactivată pentru contul dvs.
extra: Conectarea este acum posibilă folosind doar token-ul generat de aplicația TOTP asociată.
@ -84,31 +84,31 @@ ro:
subject: 'Mastodon: Autentificarea prin chei de securitate activată'
title: Chei de securitate activate
omniauth_callbacks:
failure: Nu te-am putut autentifica de la %{kind} deoarece "%{reason}".
failure: Nu v-am putut autentifica de la %{kind} deoarece "%{reason}".
success: Autentificat cu succes din contul %{kind}.
passwords:
no_token: Nu puteți accesa această pagină fără să veniți dintr-un e-mail de resetare a parolei. Dacă vii dintr-un e-mail de resetare a parolei, te rugăm să te asiguri că ai folosit URL-ul complet furnizat.
send_instructions: Dacă adresa ta de e-mail există în baza noastră de date, vei primi în câteva minute un link de recuperare a parolei la adresa ta de e-mail. Te rugăm să verifici dosarul spam dacă nu ai primit acest e-mail.
send_paranoid_instructions: Dacă adresa ta de e-mail există în baza noastră de date, vei primi în câteva minute un link de recuperare a parolei la adresa ta de e-mail. Te rugăm să verifici dosarul spam dacă nu ai primit acest e-mail.
updated: Parola ta a fost schimbată cu succes. Acum ești conectat.
updated_not_active: Parola ta a fost schimbată cu succes.
no_token: Nu puteți accesa această pagină fără să veniți dintr-un e-mail de resetare a parolei. Dacă veniți dintr-un e-mail de resetare a parolei, vă rugăm asigurați-vă că ați folosit URL-ul complet furnizat.
send_instructions: Dacă adresa dvs. de e-mail există în baza noastră de date, veți primi în câteva minute un link de recuperare a parolei la adresa dvs. de e-mail. Vă rugăm să verificați dosarul spam dacă nu ați primit acest e-mail.
send_paranoid_instructions: Dacă adresa dvs. de e-mail există în baza noastră de date, veți primi în câteva minute un link de recuperare a parolei la adresa dvs. de e-mail. Vă rugăm să verificați dosarul spam dacă nu ați primit acest e-mail.
updated: Parola dvs. a fost schimbată cu succes. Acum sunteți conectat.
updated_not_active: Parola dvs. a fost schimbată cu succes.
registrations:
destroyed: La revedere! Contul tău a fost anulat cu succes. Sperăm să te vedem din nou în curând.
destroyed: La revedere! Contul dvs. a fost anulat cu succes. Sperăm să vă vedem din nou în curând.
signed_up: Bine ați venit! V-ați înregistrat cu succes.
signed_up_but_inactive: V-ați înregistrat cu succes. Cu toate acestea, nu vă putem conecta deoarece contul dvs. nu este încă activat.
signed_up_but_locked: V-ați înregistrat cu succes. Cu toate acestea, nu vă putem conecta deoarece contul dvs. este blocat.
signed_up_but_pending: Un mesaj cu un link de confirmare a fost trimis la adresa ta de e-mail. După ce faceți clic pe link, vă vom revizui cererea. Veți fi notificat dacă este aprobat.
signed_up_but_unconfirmed: Un mesaj cu un link de confirmare a fost trimis la adresa ta de e-mail. Vă rugăm să urmați link-ul pentru a vă activa contul. Vă rugăm să verificați folderul spam dacă nu ați primit acest e-mail.
update_needs_confirmation: Ți-ai actualizat contul cu succes, dar trebuie să verificăm noua ta adresă de e-mail. Vă rugăm să verificați adresa de e-mail și să urmați link-ul de confirmare pentru a confirma noua dvs. adresă de e-mail. Te rugăm să verifici dosarul spam dacă nu ai primit acest e-mail.
signed_up_but_pending: Un mesaj cu un link de confirmare a fost trimis la adresa dvs. de e-mail. După ce faceți clic pe link, vă vom revizui cererea. Veți fi notificat dacă este aprobat.
signed_up_but_unconfirmed: Un mesaj cu un link de confirmare a fost trimis la adresa dvs. de e-mail. Vă rugăm să urmați link-ul pentru a vă activa contul. Vă rugăm să verificați folderul spam dacă nu ați primit acest e-mail.
update_needs_confirmation: V-ați actualizat contul cu succes, dar trebuie să verificăm noua dvs. adresă de e-mail. Vă rugăm să verificați adresa de e-mail și să urmați link-ul de confirmare pentru a confirma noua dvs. adresă de e-mail. Vă rugăm să verificați dosarul spam dacă nu ați primit acest e-mail.
updated: Contul dvs. a fost actualizat cu succes.
sessions:
already_signed_out: Deconectat cu succes.
signed_in: Conectat cu succes.
signed_out: Deconectat cu succes.
unlocks:
send_instructions: Veți primi un e-mail cu instrucțiuni despre cum să vă deblocați contul în câteva minute. Te rugăm să verifici dosarul spam dacă nu ai primit acest e-mail.
send_paranoid_instructions: Dacă contul tău există, vei primi un e-mail cu instrucțiuni pentru cum să-l deblochezi în câteva minute. Te rugăm să verifici dosarul spam dacă nu ai primit acest e-mail.
unlocked: Contul tău a fost deblocat cu succes. Te rugăm să te autentifici pentru a continua.
send_instructions: Veți primi un e-mail cu instrucțiuni despre cum să vă deblocați contul în câteva minute. Vă rugăm să verificați dosarul spam dacă nu ați primit acest e-mail.
send_paranoid_instructions: Dacă contul dvs. există, veți primi un e-mail cu instrucțiuni pentru cum să-l deblocați în câteva minute. Vă rugăm să verificați dosarul spam dacă nu ați primit acest e-mail.
unlocked: Contul dvs. a fost deblocat cu succes. Vă rugăm să vă autentificați pentru a continua.
errors:
messages:
already_confirmed: a fost deja confirmat, încercați să vă conectați

View file

@ -3,10 +3,10 @@ ro:
activerecord:
attributes:
doorkeeper/application:
name: Numele aplicației
name: Nume aplicație
redirect_uri: URI de redirecționare
scopes: Domenii
website: Pagina web a aplicației
website: Website aplicație
errors:
models:
doorkeeper/application:
@ -19,60 +19,60 @@ ro:
doorkeeper:
applications:
buttons:
authorize: Autorizează
cancel: Anulează
destroy: Distruge
edit: Editează
submit: Trimite
authorize: Autorizare
cancel: Anulare
destroy: Distrugere
edit: Editare
submit: Trimitere
confirmations:
destroy: Ești sigur?
destroy: Sunteți sigur?
edit:
title: Editați aplicația
title: Editare aplicație
form:
error: Ups! Verificați formularul pentru posibile erori
help:
native_redirect_uri: Utilizați %{native_redirect_uri} pentru teste locale
redirect_uri: Folosește câte o linie per URI
redirect_uri: Folosiți câte o linie per URI
scopes: Separați domeniile cu spații. Lăsați necompletat pentru a utiliza domeniile implicite.
index:
application: Aplicație
callback_url: URL pentru callback
delete: Șterge
callback_url: Callback URL
delete: Ștergere
empty: Nu aveți aplicații.
name: Nume
new: Aplicație nouă
scopes: Domenii
show: Arată
title: Aplicațiile tale
show: Afișare
title: Aplicațiile dvs.
new:
title: Aplicație nouă
show:
actions: Acțiuni
application_id: Cheie client
callback_urls: URL-uri de callback
callback_urls: Callback URL-uri
scopes: Domenii
secret: Codul secret al clientului
secret: Secretul clientului
title: 'Aplicație: %{name}'
authorizations:
buttons:
authorize: Autorizează
deny: Interzice
authorize: Autorizare
deny: Refuzare
error:
title: A apărut o eroare
new:
prompt_html: "%{client_name} dorește să îți acceseze contul. Este o aplicație terță. <strong>Dacă nu aveți încredere în ea, atunci nu ar trebui să o autorizați.</strong>"
review_permissions: Revizuiește permisiunile
review_permissions: Revizuiți permisiunile
title: Autorizare necesară
show:
title: Copiați acest cod de autorizare și lipiți-l în aplicație.
authorized_applications:
buttons:
revoke: Revocați
revoke: Revocare
confirmations:
revoke: Ești sigur?
revoke: Sunteți sigur?
index:
authorized_at: Autorizat pe %{date}
description_html: Acestea sunt aplicațiile care vă pot accesa contul folosind API. Dacă există aplicații pe care nu le recunoașteți, sau o aplicație se comportă necorespunzător, puteți revoca accesul.
description_html: Acestea sunt aplicațiile care vă pot accesa contul folosind API-ul. Dacă există aplicații pe care nu le recunoașteți, sau o aplicație se comportă necorespunzător, puteți revoca accesul.
last_used_at: Utilizat ultima dată pe %{date}
never_used: Nu a fost folosit niciodată
scopes: Permisiuni
@ -86,9 +86,9 @@ ro:
invalid_grant: Acordarea autorizației furnizată este invalidă, expirată, revocată, nu corespunde URI-ului de redirecționare folosit în cererea de autorizare, sau a fost eliberat altui client.
invalid_redirect_uri: Uri-ul de redirecționare inclus nu este valid.
invalid_request:
missing_param: 'Lipseste parametrul necesar: %{value}.'
missing_param: 'Lipsește parametrul necesar: %{value}.'
request_not_authorized: Solicitarea trebuie să fie autorizată. Parametrul necesar pentru solicitarea de autorizare lipsește sau este invalid.
unknown: Solicitarea nu are un parametru necesar, include un parametru nesuportat sau este dealtfel formatat incorect.
unknown: Solicitarea nu are un parametru necesar, include un parametru nesuportat sau este formatat incorect.
invalid_resource_owner: Acreditările proprietarului de resurse nu sunt valide sau proprietarul de resurse nu poate fi găsit
invalid_scope: Domeniul de aplicare solicitat este invalid, necunoscut sau incorect.
invalid_token:
@ -137,12 +137,12 @@ ro:
notifications: Notificări
push: Notificări push
reports: Rapoarte
search: Caută
search: Căutare
statuses: Postări
layouts:
admin:
nav:
applications: Aplicaţii
applications: Aplicații
oauth2_provider: Furnizor OAuth2
application:
title: Este necesară autorizarea OAuth
@ -160,7 +160,7 @@ ro:
read:accounts: vede informațiile privind conturile
read:blocks: vede blocurile tale
read:bookmarks: vede marcajele tale
read:favourites: vezi favoritele tale
read:favourites: vede favoritele tale
read:filters: vede filtrele tale
read:follows: vede urmăririle tale
read:lists: vede listele tale
@ -168,7 +168,7 @@ ro:
read:notifications: vede notificările tale
read:reports: vede raportările tale
read:search: caută în numele tău
read:statuses: vede toate stările
read:statuses: vede toate postările
write: modifică toate datele contului tău
write:accounts: modifică profilul tău
write:blocks: blochează conturile și domeniile

View file

@ -604,7 +604,7 @@ es-MX:
suspend_description_html: La cuenta y todos sus contenidos serán inaccesibles y eventualmente eliminados, e interactuar con ella será imposible. Reversible durante 30 días. Cierra todos los reportes contra esta cuenta.
actions_description_html: Decide qué medidas tomar para resolver esta denuncia. Si tomas una acción punitiva contra la cuenta denunciada, se le enviará a dicha cuenta una notificación por correo electrónico, excepto cuando se seleccione la categoría <strong>Spam</strong>.
actions_description_remote_html: Decide qué medidas tomar para resolver este reporte. Esto solo afectará a la forma en que <strong>tu servidor</strong> se comunica con esta cuenta remota y gestiona su contenido.
actions_no_posts: Este informe no incluye ninguna publicación asociada a eliminar
actions_no_posts: Este informe no tiene ningún mensaje asociado para eliminar
add_to_report: Añadir más al reporte
already_suspended_badges:
local: Ya suspendido en este servidor
@ -911,8 +911,8 @@ es-MX:
trends:
allow: Permitir
approved: Aprobado
confirm_allow: "¿Estás seguro de que deseas permitir la etiqueta seleccionada?"
confirm_disallow: "¿Estás seguro de que deseas restringir la etiqueta seleccionada?"
confirm_allow: "¿Estás seguro de que deseas permitir las etiquetas seleccionadas?"
confirm_disallow: "¿Estás seguro de que deseas restringir las etiquetas seleccionadas?"
disallow: Rechazar
links:
allow: Permitir enlace
@ -980,7 +980,7 @@ es-MX:
used_by_over_week:
one: Usada por una persona durante la última semana
other: Usada por %{count} personas durante la última semana
title: Recomendaciones y tendencias
title: Recomendaciones y Tendencias
trending: En tendencia
warning_presets:
add_new: Añadir nuevo

View file

@ -604,7 +604,7 @@ es:
suspend_description_html: La cuenta y todos sus contenidos serán inaccesibles y finalmente eliminados, e interactuar con ella será imposible. Reversible durante 30 días. Cierra todos los informes contra esta cuenta.
actions_description_html: Decide qué medidas tomar para resolver esta denuncia. Si tomas una acción punitiva contra la cuenta denunciada, se le enviará a dicha cuenta una notificación por correo electrónico, excepto cuando se seleccione la categoría <strong>Spam</strong>.
actions_description_remote_html: Decide qué medidas tomar para resolver este informe. Esto solo afectará a la forma en que <strong>tu servidor</strong> se comunica con esta cuenta remota y gestiona su contenido.
actions_no_posts: Este informe no incluye ninguna publicación asociada a eliminar
actions_no_posts: Este informe no tiene ningún mensaje asociado para eliminar
add_to_report: Añadir más al reporte
already_suspended_badges:
local: Ya suspendido en este servidor
@ -911,8 +911,8 @@ es:
trends:
allow: Permitir
approved: Aprobadas
confirm_allow: "¿Estás seguro de que deseas permitir la etiqueta seleccionada?"
confirm_disallow: "¿Estás seguro de que deseas restringir la etiqueta seleccionada?"
confirm_allow: "¿Estás seguro de que deseas permitir las etiquetas seleccionadas?"
confirm_disallow: "¿Estás seguro de que deseas restringir las etiquetas seleccionadas?"
disallow: No permitir
links:
allow: Permitir enlace
@ -980,7 +980,7 @@ es:
used_by_over_week:
one: Usada por una persona durante la última semana
other: Usada por %{count} personas durante la última semana
title: Recomendaciones y tendencias
title: Recomendaciones y Tendencias
trending: En tendencia
warning_presets:
add_new: Añadir nuevo

View file

@ -24,6 +24,7 @@ et:
admin:
account_actions:
action: Täida tegevus
already_silenced: See konto on juba piiratud.
already_suspended: See konto on juba peatatud.
title: Rakenda moderaatori tegevus kasutajale %{acct}
account_moderation_notes:
@ -133,6 +134,7 @@ et:
resubscribe: Telli taas
role: Roll
search: Otsi
search_same_email_domain: Muud kasutajad sama e-posti domeeniga
search_same_ip: Teised kasutajad, kellel on sama IP
security: Turvalisus
security_measures:
@ -173,21 +175,26 @@ et:
approve_appeal: Rahulda vaidlustus
approve_user: Kinnita kasutaja
assigned_to_self_report: Määras Teavituse
change_email_user: Muuda kasutaja e-posti
change_role_user: Muuda kasutaja rolli
confirm_user: Kasutaja kinnitatud
create_account_warning: Lisas hoiatuse
create_announcement: Lisas teadaande
create_canonical_email_block: Loo e-posti blokeering
create_custom_emoji: Lisas kohandatud emotikoni
create_domain_allow: Lisas lubatud domeeni
create_domain_block: Domeeni blokeerimine
create_email_domain_block: Loo e-posti domeeni blokeering
create_ip_block: IP-reegli lisamine
create_unavailable_domain: Kättesaamatu domeeni lisamine
create_user_role: Loo roll
demote_user: Alandas kasutaja
destroy_announcement: Eemaldas teadaande
destroy_canonical_email_block: Kustuta e-posti blokeering
destroy_custom_emoji: Eemaldas kohandatud emotikoni
destroy_domain_allow: Eemaldas lubatud domeeni
destroy_domain_block: Domeeniblokeeringu eemaldamine
destroy_email_domain_block: Kustuta e-posti domeeni blokeering
destroy_instance: Domeeni kustutamine
destroy_ip_block: IP-reegli kustutamine
destroy_status: Kustuta postitus
@ -228,20 +235,26 @@ et:
approve_appeal_html: "%{name} kiitis heaks modereerimise otsuse vaidlustuse %{target} poolt"
approve_user_html: "%{name} kiitis heaks registreerimise %{target} poolt"
assigned_to_self_report_html: "%{name} määras raporti %{target} endale"
change_email_user_html: "%{name} muutis kasutaja %{target} e-postiaadressi"
change_role_user_html: "%{name} muutis %{target} rolli"
confirm_user_html: "%{name} kinnitas kasutaja %{target} e-postiaadressi"
create_account_warning_html: "%{name} saatis %{target} hoiatuse"
create_announcement_html: "%{name} lõi uue teate %{target}"
create_canonical_email_block_html: "%{name} blokeeris e-posti räsiga %{target}"
create_custom_emoji_html: "%{name} laadis üles uue emotikoni %{target}"
create_domain_allow_html: "%{name} lubas föderatsiooni domeeniga %{target}"
create_domain_block_html: "%{name} keelas domeeni %{target}"
create_email_domain_block_html: "%{name} blokeeris e-posti domeeni %{target}"
create_ip_block_html: "%{name} lõi IP-aadressile %{target} reegli"
create_unavailable_domain_html: "%{name} lõpetas edastamise domeeni %{target}"
create_user_role_html: "%{name} lõi rolli %{target}"
demote_user_html: "%{name} alandas kasutajat %{target}"
destroy_announcement_html: "%{name} kustutas teadaande %{target}"
destroy_canonical_email_block_html: "%{name} eemaldas blokeeringu e-postilt räsiga %{target}"
destroy_custom_emoji_html: "%{name} kustutas emotikoni %{target}"
destroy_domain_allow_html: "%{name} keelas föderatsiooni domeeniga %{target}"
destroy_domain_block_html: "%{name} lubas domeeni %{target}"
destroy_email_domain_block_html: "%{name} eemaldas blokeeringu e-posti domeenilt %{target}"
destroy_instance_html: "%{name} kustutas domeeni %{target}"
destroy_ip_block_html: "%{name} kustutas IP-aadressi %{target} reegli"
destroy_status_html: "%{name} kustutas %{target} postituse"
@ -260,6 +273,7 @@ et:
reject_user_html: "%{name} lükkas %{target} liitumissoovi tagasi"
remove_avatar_user_html: "%{name} eemaldas %{target} avatari"
reopen_report_html: "%{name} taasavas raporti %{target}"
resend_user_html: "%{name} lähtestas %{target} kinnituskirja e-posti"
reset_password_user_html: "%{name} lähtestas %{target} salasõna"
resolve_report_html: "%{name} lahendas raporti %{target}"
sensitive_account_html: "%{name} märkis %{target} meedia kui tundlik sisu"
@ -420,6 +434,7 @@ et:
attempts_over_week:
one: "%{count} katse viimase nädala kestel"
other: "%{count} liitumiskatset viimase nädala kestel"
created_msg: E-posti domeen edukalt blokeeritud
delete: Kustuta
dns:
types:
@ -428,8 +443,12 @@ et:
new:
create: Lisa domeen
resolve: Domeeni lahendamine
title: Blokeeri uus e-posti domeen
no_email_domain_block_selected: Ühtegi e-posti domeeni blokeeringut ei muudetud, kuna ühtegi ei valitud
not_permitted: Ei ole lubatud
resolved_dns_records_hint_html: Domeeninimi lahendatakse järgmistele MX-domeenidele, mis on lõppkokkuvõttes vastutavad e-kirjade vastuvõtmise eest. MX-domeeni blokeerimine blokeerib registreerimise mis tahes e-posti aadressilt, mis kasutab sama MX-domeeni, isegi kui nähtav domeeninimi on erinev. <strong>Ole ettevaatlik, et mitte blokeerida peamisi e-posti teenusepakkujaid.</strong>
resolved_through_html: Lahendatud %{domain} kaudu
title: Blokeeritud e-posti domeenid
export_domain_allows:
new:
title: Lubatud domeenide import
@ -583,6 +602,7 @@ et:
resolve_description_html: Raporteeritud konto suhtes ei võeta midagi ette, juhtumit ei registreerita ja raport suletakse.
silence_description_html: Konto saab olema nähtav ainult senistele jälgijatele või otsestele pöördujatele, mõjutates avalikku levi. On tagasipööratav. Sulgeb kõik konto suhtes esitatud raportid.
suspend_description_html: See konto ja kogu selle sisu muutub kättesaamatuks ning kustub lõpuks ja igasugune suhtlus sellega muutub võimatuks. Tagasipööratav 30 päeva jooksul. Lõpetab kõik selle konto kohta esitatud kaebused.
actions_description_html: Otsusta, milliseid meetmeid selle raporti lahendamiseks võtta. Kui võtad raporteeritud konto suhtes karistusmeetme, saadetakse talle e-posti teade, välja arvatud juhul, kui valid kategooria <strong>Rämps</strong>.
actions_description_remote_html: Otsusta, mida teha selle raporti lahendamiseks. See mõjutab ainult seda, kuidas <strong>Sinu</strong> server selle kaugkontoga suhtleb ning selle sisu käsitleb.
actions_no_posts: Selle raportiga pole seotud ühtegi postitust, mida kustutada
add_to_report: Lisa raportile juurde
@ -648,6 +668,7 @@ et:
delete_data_html: Kustuta tänasest 30 päeva pärast kasutaja <strong>@%{acct}</strong> profiil ja sisu, kui vahepeal tema kontot ei taastata
preview_preamble_html: "<strong>@%{acct}</strong> saab järgmise sisuga hoiatuse:"
record_strike_html: Salvesta <strong>@%{acct}</strong> kohta juhtum, et aidata selle konto tulevaste rikkumiste puhul reageerida
send_email_html: Saada hoiatuskiri <strong>@%{acct}</strong>
warning_placeholder: Valikuline täiendav põhjendus modereerimisele.
target_origin: Raporteeritud konto päritolu
title: Teavitused
@ -687,6 +708,7 @@ et:
manage_appeals: Vaidlustuste haldamine
manage_appeals_description: Lubab kasutajail läbi vaadata modereerimisotsuste vaidlustusi
manage_blocks: Keeldude haldamine
manage_blocks_description: Lubab kasutajatel blokeerida e-posti teenusepakkujaid ja IP-aadresse
manage_custom_emojis: Halda isetehtud emotikone
manage_custom_emojis_description: Lubab kasutajatel hallata serveris isetehtud emotikone
manage_federation: Halda födereerumist
@ -704,6 +726,7 @@ et:
manage_taxonomies: Halda taksonoomiaid
manage_taxonomies_description: Luba kasutajatel populaarset sisu üle vaadata ning uuendada siltide sätteid
manage_user_access: Halda kasutajate ligipääsu
manage_user_access_description: Võimaldab kasutajatel keelata teiste kasutajate kaheastmelise autentimise, muuta oma e-posti aadressi ja lähtestada oma parooli
manage_users: Kasutajate haldamine
manage_users_description: Lubab kasutajail näha teiste kasutajate üksikasju ja teha nende suhtes modereerimisotsuseid
manage_webhooks: Halda webhook'e
@ -1137,6 +1160,12 @@ et:
view_strikes: Vaata enda eelnevaid juhtumeid
too_fast: Vorm esitatud liiga kiirelt, proovi uuesti.
use_security_key: Kasuta turvavõtit
author_attribution:
example_title: Näidistekst
hint_html: Määra, kuidas sind krediteeritakse, kui linke Mastodonis jagatakse.
more_from_html: Rohkem kasutajalt %{name}
s_blog: Kasutaja %{name} blogi
title: Autori tunnustamine
challenge:
confirm: Jätka
hint_html: "<strong>Nõuanne:</strong> Me ei küsi salasõna uuesti järgmise tunni jooksul."
@ -1418,6 +1447,16 @@ et:
unsubscribe:
action: Jah, lõpeta tellimine
complete: Tellimine lõpetatud
confirmation_html: Kas oled kindel, et soovid loobuda %{type} tellimisest oma e-postiaadressile %{email} Mastodonist kohas %{domain}? Saad alati uuesti tellida oma <a href="%{settings_path}">e-posti teavituste seadetest</a>.
emails:
notification_emails:
favourite: lemmikuks märkimise teavituskirjade
follow: jälgimiste teavituskirjade
follow_request: jälgimistaotluste teavituskirjade
mention: mainimiste teavituskirjade
reblog: jagamiste teavituskirjade
resubscribe_html: Kui loobusid tellimisest ekslikult, saad uuesti tellida oma <a href="%{settings_path}">e-posti teavituste seadetest</a>.
success_html: Sa ei saa enam %{type} teateid oma e-postile %{email} Mastodonist kohas %{domain}.
title: Loobu tellimisest
media_attachments:
validations:
@ -1906,6 +1945,7 @@ et:
instructions_html: Kopeeri ja kleebi allpool olev kood oma lehe HTML lähtekoodi. Seejärel lisa oma kodulehe aadress profiili "Muuda profiili" taabi ühte lisavälja ning salvesta muudatused.
verification: Kinnitamine
verified_links: Sinu kontrollitud lingid
website_verification: Veebilehe kontrollimine
webauthn_credentials:
add: Uue turvavõtme lisamine
create:

View file

@ -24,6 +24,7 @@ fr-CA:
admin:
account_actions:
action: Effectuer l'action
already_silenced: Ce compte a déjà été limité.
already_suspended: Ce compte est déjà suspendu.
title: Effectuer une action de modération sur %{acct}
account_moderation_notes:
@ -133,7 +134,7 @@ fr-CA:
resubscribe: Se réabonner
role: Rôle
search: Rechercher
search_same_email_domain: Autres utilisateurs avec le même domaine de courriel
search_same_email_domain: Autres utilisateur·rice·s ayant le même domaine de messagerie
search_same_ip: Autres utilisateur·rice·s avec la même IP
security: Sécurité
security_measures:
@ -270,6 +271,7 @@ fr-CA:
reject_user_html: "%{name} a rejeté linscription de %{target}"
remove_avatar_user_html: "%{name} a supprimé l'avatar de %{target}"
reopen_report_html: "%{name} a rouvert le signalement %{target}"
resend_user_html: "%{name} a renvoyé l'e-mail de confirmation pour %{target}"
reset_password_user_html: "%{name} a réinitialisé le mot de passe de l'utilisateur·rice %{target}"
resolve_report_html: "%{name} a résolu le signalement %{target}"
sensitive_account_html: "%{name} a marqué le média de %{target} comme sensible"
@ -284,6 +286,7 @@ fr-CA:
update_custom_emoji_html: "%{name} a mis à jour l'émoji %{target}"
update_domain_block_html: "%{name} a mis à jour le blocage de domaine pour %{target}"
update_ip_block_html: "%{name} a modifié la règle pour l'IP %{target}"
update_report_html: "%{name} a mis à jour le rapport de signalement %{target}"
update_status_html: "%{name} a mis à jour le message de %{target}"
update_user_role_html: "%{name} a changé le rôle %{target}"
deleted_account: compte supprimé
@ -439,7 +442,12 @@ fr-CA:
create: Créer le blocage
resolve: Résoudre le domaine
title: Blocage d'un nouveau domaine de messagerie électronique
no_email_domain_block_selected: Aucun blocage de domaine de messagerie n'a été modifié comme aucun n'a été sélectionné
not_permitted: Non autorisé
resolved_dns_records_hint_html: |-
Le nom de domaine se réfère aux domaines MX suivants, qui sont à leur tour responsables de la réception des courriels.
Le blocage d'un domaine MX empêchera l'inscription depuis toute adresse électronique ayant recours au même domaine MX, et ce même si le nom de domaine visible est différent. <strong>Veillez à ne pas bloquer les principaux fournisseurs de services de messagerie.</strong>
resolved_through_html: Résolu par %{domain}
title: Domaines de messagerie électronique bloqués
export_domain_allows:
@ -595,7 +603,9 @@ fr-CA:
resolve_description_html: Aucune mesure ne sera prise contre le compte signalé, aucune sanction ne sera enregistrée et le sigalement sera clôturé.
silence_description_html: Le compte ne sera visible que par ceux qui le suivent déjà ou qui le recherchent manuellement, ce qui limite fortement sa portée. Cette action peut toujours être annulée. Cloture tous les signalements concernant ce compte.
suspend_description_html: Le compte et tous ses contenus seront inaccessibles et finalement supprimés, et il sera impossible d'interagir avec lui. Réversible dans les 30 jours. Cloture tous les signalements concernant ce compte.
actions_description_html: Décidez de l'action à entreprendre pour résoudre ce signalement. Si vous prenez une mesure punitive à l'encontre du compte signalé, une notification par courrier électronique lui sera envoyée, excepté lorsque la catégorie <strong>Spam</strong> est sélectionnée.
actions_description_remote_html: Décidez des mesures à prendre pour résoudre ce signalement. Cela n'affectera que la manière dont <strong>votre</strong> serveur communique avec ce compte distant et traite son contenu.
actions_no_posts: Ce signalement n'a pas de messages qui lui sont associés et qui devraient être supprimés
add_to_report: Ajouter davantage au rapport
already_suspended_badges:
local: Déjà suspendu sur ce serveur
@ -717,6 +727,7 @@ fr-CA:
manage_taxonomies: Gérer les taxonomies
manage_taxonomies_description: Permet aux utilisateur⋅rice⋅s d'examiner les contenus tendance et de mettre à jour les paramètres des hashtags
manage_user_access: Gérer l'accès utilisateur
manage_user_access_description: Permet aux utilisateur·rice·s de désactiver l'authentification à deux facteurs des autres utilisateur·rice·s, de modifier leur adresse électronique et de réinitialiser leur mot de passe
manage_users: Gérer les utilisateur·rice·s
manage_users_description: Permet aux utilisateur⋅rice⋅s de voir les détails des autres utilisateur⋅rice⋅s et d'effectuer des actions de modération en conséquence
manage_webhooks: Gérer les points dancrage web
@ -879,16 +890,19 @@ fr-CA:
message_html: "<strong>Votre serveur web est mal configuré. La confidentialité de vos utilisateurs est en péril.</strong>"
tags:
moderation:
not_trendable: Ne peut être en tendance
not_usable: Non utilisable
pending_review: En attente de traitement
review_requested: Révision requise
reviewed: Traité
title: État
trendable: Peut s'afficher dans les tendances
unreviewed: Non traité
usable: Utilisable
name: Nom
newest: Plus récents
oldest: Plus anciens
open: Afficher publiquement
reset: Réinitialiser
review: État du traitement
search: Recherche
@ -1434,6 +1448,15 @@ fr-CA:
unsubscribe:
action: Oui, me désabonner
complete: Désabonné·e
emails:
notification_emails:
favourite: e-mails de notifications de favoris
follow: e-mails de notifications dabonnements
follow_request: e-mails de demandes dabonnements
mention: e-mails de notifications de mentions
reblog: e-mails de notifications de partages
resubscribe_html: Si vous vous êtes désinscrit par erreur, vous pouvez vous réinscrire à partir de vos <a href="%{settings_path}">paramètres de notification par e-mail</a>.
success_html: Vous ne recevrez plus de %{type} pour Mastodon sur %{domain} à votre adresse e-mail à %{email}.
title: Se désabonner
media_attachments:
validations:

View file

@ -24,6 +24,7 @@ fr:
admin:
account_actions:
action: Effectuer l'action
already_silenced: Ce compte a déjà été limité.
already_suspended: Ce compte est déjà suspendu.
title: Effectuer une action de modération sur %{acct}
account_moderation_notes:
@ -133,7 +134,7 @@ fr:
resubscribe: Se réabonner
role: Rôle
search: Rechercher
search_same_email_domain: Autres utilisateurs avec le même domaine de courriel
search_same_email_domain: Autres utilisateur·rice·s ayant le même domaine de messagerie
search_same_ip: Autres utilisateur·rice·s avec la même IP
security: Sécurité
security_measures:
@ -270,6 +271,7 @@ fr:
reject_user_html: "%{name} a rejeté linscription de %{target}"
remove_avatar_user_html: "%{name} a supprimé l'avatar de %{target}"
reopen_report_html: "%{name} a rouvert le signalement %{target}"
resend_user_html: "%{name} a renvoyé l'e-mail de confirmation pour %{target}"
reset_password_user_html: "%{name} a réinitialisé le mot de passe de l'utilisateur·rice %{target}"
resolve_report_html: "%{name} a résolu le signalement %{target}"
sensitive_account_html: "%{name} a marqué le média de %{target} comme sensible"
@ -284,6 +286,7 @@ fr:
update_custom_emoji_html: "%{name} a mis à jour l'émoji %{target}"
update_domain_block_html: "%{name} a mis à jour le blocage de domaine pour %{target}"
update_ip_block_html: "%{name} a modifié la règle pour l'IP %{target}"
update_report_html: "%{name} a mis à jour le rapport de signalement %{target}"
update_status_html: "%{name} a mis à jour le message de %{target}"
update_user_role_html: "%{name} a changé le rôle %{target}"
deleted_account: compte supprimé
@ -439,7 +442,12 @@ fr:
create: Créer le blocage
resolve: Résoudre le domaine
title: Blocage d'un nouveau domaine de messagerie électronique
no_email_domain_block_selected: Aucun blocage de domaine de messagerie n'a été modifié comme aucun n'a été sélectionné
not_permitted: Non autorisé
resolved_dns_records_hint_html: |-
Le nom de domaine se réfère aux domaines MX suivants, qui sont à leur tour responsables de la réception des courriels.
Le blocage d'un domaine MX empêchera l'inscription depuis toute adresse électronique ayant recours au même domaine MX, et ce même si le nom de domaine visible est différent. <strong>Veillez à ne pas bloquer les principaux fournisseurs de services de messagerie.</strong>
resolved_through_html: Résolu par %{domain}
title: Domaines de messagerie électronique bloqués
export_domain_allows:
@ -595,7 +603,9 @@ fr:
resolve_description_html: Aucune mesure ne sera prise contre le compte signalé, aucune sanction ne sera enregistrée et le sigalement sera clôturé.
silence_description_html: Le compte ne sera visible que par ceux qui le suivent déjà ou qui le recherchent manuellement, ce qui limite fortement sa portée. Cette action peut toujours être annulée. Cloture tous les signalements concernant ce compte.
suspend_description_html: Le compte et tous ses contenus seront inaccessibles et finalement supprimés, et il sera impossible d'interagir avec lui. Réversible dans les 30 jours. Cloture tous les signalements concernant ce compte.
actions_description_html: Décidez de l'action à entreprendre pour résoudre ce signalement. Si vous prenez une mesure punitive à l'encontre du compte signalé, une notification par courrier électronique lui sera envoyée, excepté lorsque la catégorie <strong>Spam</strong> est sélectionnée.
actions_description_remote_html: Décidez des mesures à prendre pour résoudre ce signalement. Cela n'affectera que la manière dont <strong>votre</strong> serveur communique avec ce compte distant et traite son contenu.
actions_no_posts: Ce signalement n'a pas de messages qui lui sont associés et qui devraient être supprimés
add_to_report: Ajouter davantage au rapport
already_suspended_badges:
local: Déjà suspendu sur ce serveur
@ -717,6 +727,7 @@ fr:
manage_taxonomies: Gérer les taxonomies
manage_taxonomies_description: Permet aux utilisateur⋅rice⋅s d'examiner les contenus tendance et de mettre à jour les paramètres des hashtags
manage_user_access: Gérer l'accès utilisateur
manage_user_access_description: Permet aux utilisateur·rice·s de désactiver l'authentification à deux facteurs des autres utilisateur·rice·s, de modifier leur adresse électronique et de réinitialiser leur mot de passe
manage_users: Gérer les utilisateur·rice·s
manage_users_description: Permet aux utilisateur⋅rice⋅s de voir les détails des autres utilisateur⋅rice⋅s et d'effectuer des actions de modération en conséquence
manage_webhooks: Gérer les points dancrage web
@ -879,16 +890,19 @@ fr:
message_html: "<strong>Votre serveur web est mal configuré. La confidentialité de vos utilisateurs est en péril.</strong>"
tags:
moderation:
not_trendable: Ne peut être en tendance
not_usable: Non utilisable
pending_review: En attente de traitement
review_requested: Révision requise
reviewed: Traité
title: État
trendable: Peut s'afficher dans les tendances
unreviewed: Non traité
usable: Utilisable
name: Nom
newest: Plus récents
oldest: Plus anciens
open: Afficher publiquement
reset: Réinitialiser
review: État du traitement
search: Recherche
@ -1434,6 +1448,15 @@ fr:
unsubscribe:
action: Oui, se désinscrire
complete: Désinscrit
emails:
notification_emails:
favourite: e-mails de notifications de favoris
follow: e-mails de notifications dabonnements
follow_request: e-mails de demandes dabonnements
mention: e-mails de notifications de mentions
reblog: e-mails de notifications de partages
resubscribe_html: Si vous vous êtes désinscrit par erreur, vous pouvez vous réinscrire à partir de vos <a href="%{settings_path}">paramètres de notification par e-mail</a>.
success_html: Vous ne recevrez plus de %{type} pour Mastodon sur %{domain} à votre adresse e-mail à %{email}.
title: Se désinscrire
media_attachments:
validations:

View file

@ -30,6 +30,7 @@ ga:
admin:
account_actions:
action: Déan gníomh
already_silenced: Tá teorainn leis an gcuntas seo cheana féin.
already_suspended: Tá an cuntas seo curtha ar fionraí cheana féin.
title: Dean gníomh modhnóireachta ar %{acct}
account_moderation_notes:
@ -1213,6 +1214,12 @@ ga:
view_strikes: Féach ar stailceanna san am atá caite i gcoinne do chuntais
too_fast: Cuireadh an fhoirm isteach róthapa, triail arís.
use_security_key: Úsáid eochair shlándála
author_attribution:
example_title: Téacs samplach
hint_html: Rialú conas a chuirtear chun sochair tú nuair a roinntear naisc ar Mastodon.
more_from_html: Tuilleadh ó %{name}
s_blog: Blag %{name}
title: Leithdháil an údair
challenge:
confirm: Lean ar aghaidh
hint_html: "<strong>Leid:</strong> Ní iarrfaimid do phasfhocal ort arís go ceann uair an chloig eile."
@ -2029,6 +2036,7 @@ ga:
instructions_html: Cóipeáil agus greamaigh an cód thíos isteach i HTML do shuíomh Gréasáin. Ansin cuir seoladh do shuíomh Gréasáin isteach i gceann de na réimsí breise ar do phróifíl ón gcluaisín "Cuir próifíl in eagar" agus sábháil athruithe.
verification: Fíorú
verified_links: Do naisc fhíoraithe
website_verification: Fíorú láithreán gréasáin
webauthn_credentials:
add: Cuir eochair shlándála nua leis
create:

View file

@ -28,6 +28,7 @@ gd:
admin:
account_actions:
action: Gabh an gnìomh
already_silenced: Chaidh an cunntas seo a chuingeachadh mu thràth.
already_suspended: Chaidh an cunntas seo a chur à rèim mu thràth.
title: Gabh gnìomh maorsainneachd air %{acct}
account_moderation_notes:
@ -1195,6 +1196,12 @@ gd:
view_strikes: Seall na rabhaidhean a fhuair an cunntas agad roimhe
too_fast: Chaidh am foirm a chur a-null ro luath, feuch ris a-rithist.
use_security_key: Cleachd iuchair tèarainteachd
author_attribution:
example_title: Ball-sampaill teacsa
hint_html: Stùirich mar a thèid iomradh a thoirt ort nuair a thèid ceangal a cho-roinneadh air Mastodon.
more_from_html: Barrachd o %{name}
s_blog: Bloga aig %{name}
title: Aithris air an ùghdar
challenge:
confirm: Lean air adhart
hint_html: "<strong>Gliocas:</strong> Chan iarr sinn am facal-faire agad ort a-rithist fad uair a thìde."
@ -1998,6 +2005,7 @@ gd:
instructions_html: Dèan lethbhreac dhen chòd gu h-ìosal is cuir a-steach ann an HTML na làraich-lìn agad e. An uairsin, cuir seòladh na làraich-lìn agad ri fear dhe na raointean a bharrachd air a phròifil agad o thaba “Deasaich a phròifil” agus sàbhail na h-atharraichean.
verification: Dearbhadh
verified_links: Na ceanglaichean dearbhte agad
website_verification: Dearbhadh làraich-lìn
webauthn_credentials:
add: Cuir iuchair tèarainteachd ùr ris
create:

View file

@ -421,9 +421,21 @@ lt:
domain: Domenas
new:
create: Pridėto domeną
export_domain_allows:
new:
title: Importuoti domeno leidžiamus
no_file: Nėra pasirinkto failo
export_domain_blocks:
import:
description_html: Netrukus importuosi domenų blokavimų sąrašą. Labai atidžiai peržiūrėk šį sąrašą, ypač jei ne tu jį sudarei.
existing_relationships_warning: Esami sekimo sąryšiai
private_comment_description_html: 'Kad būtų lengviau atsekti, iš kur importuoti blokavimai, importuoti blokavimai bus kuriami su šiuo privačiu komentaru: <q>%{comment}</q>'
private_comment_template: Importuota iš %{source}, %{date}
title: Importuoti domeno blokavimus
invalid_domain_block: 'Vienas ar daugiau domenų blokavimų buvo praleisti dėl toliau nurodytos (-ų) klaidos (-ų): %{error}'
new:
title: Importuoti domeno blokavimus
no_file: Nėra pasirinkto failo
follow_recommendations:
language: Kalbui
status: Būsena
@ -853,18 +865,18 @@ lt:
exports:
archive_takeout:
date: Data
download: Parsisiųsti archyvą
hint_html: Jūs galite prašyti savo <strong>įrašų bei medijos</strong> archyvo. Eksportuota informacija bus ActivityPub formatu, skaitoma suderintų programų. Galite prašyti archyvo, kas 7 dienas.
download: Atsisiųsti archyvą
hint_html: Gali paprašyti savo <strong>įrašų ir įkeltos medijos</strong> archyvo. Eksportuoti duomenys bus „ActivityPub“ formatu, kurį galima perskaityti bet kuria suderinama programine įranga. Archyvo galima prašyti kas 7 dienas.
in_progress: Sudaromas archyvas...
request: Prašyti savo archyvo
size: Dydis
blocks: Jūs blokuojate
blocks: Blokuoji
bookmarks: Žymės
csv: CSV
domain_blocks: Domeno blokai
domain_blocks: Domeno blokavimai
lists: Sąrašai
mutes: Jūs tildote
storage: Medijos sandėlis
mutes: Nutildei
storage: Medijos saugykla
featured_tags:
add_new: Pridėti naują
hint_html: "<strong>Savo profilyje parodyk svarbiausius saitažodžius.</strong> Tai puikus įrankis kūrybiniams darbams ir ilgalaikiams projektams sekti, todėl svarbiausios saitažodžiai rodomi matomoje vietoje profilyje ir leidžia greitai pasiekti tavo paties įrašus."
@ -904,7 +916,7 @@ lt:
merge_long: Išsaugoti esančius įrašus ir pridėti naujus
overwrite: Perrašyti
overwrite_long: Pakeisti senus įrašus naujais
preface: Jūs galite importuoti informaciją iš kito serverio, tokią kaip sąrašą žmonių kuriuos sekate.
preface: Gali importuoti duomenis, kuriuos eksportavai iš kito serverio, pavyzdžiui, sekamų arba blokuojamų žmonių sąrašą.
success: Jūsų informacija sėkmingai įkelta ir bus apdorota kaip įmanoma greičiau
types:
blocking: Blokuojamų sąrašas
@ -955,6 +967,8 @@ lt:
migrations:
acct: Perkelta į
cancel: Atšaukti nukreipimą
warning:
disabled_account: Po to tavo dabartine paskyra nebus galima naudotis visiškai. Tačiau turėsi prieigą prie duomenų eksporto ir pakartotinio aktyvavimo.
moderation:
title: Prižiūrėjimas
notification_mailer:
@ -1079,6 +1093,7 @@ lt:
export: Duomenų eksportas
featured_tags: Rodomi saitažodžiai
import: Importuoti
import_and_export: Importas ir eksportas
migrate: Paskyros migracija
notifications: El. laiško pranešimai
preferences: Nuostatos

View file

@ -461,6 +461,9 @@ lv:
title: Sekošanas ieteikumi
unsuppress: Atjaunot sekošanas rekomendāciju
instances:
audit_log:
title: Nesenie pārbaudes žurnāli
view_all: Skatīt pilnus pārbaudes žurnālus
availability:
description_html:
one: Ja piegāde uz domēnu neizdodas <strong>%{count} dienu</strong> bez panākumiem, turpmāki piegādes mēģinājumi netiks veikti, ja vien netiks saņemta piegāde <em>no</em> domēna.
@ -591,7 +594,9 @@ lv:
resolve_description_html: Pret norādīto kontu netiks veiktas nekādas darbības, netiks reģistrēts brīdinājums, un ziņojums tiks slēgts.
silence_description_html: Konts būs redzams tikai tiem, kas tam jau seko vai meklē to manuāli, ievērojami ierobežojot tā sasniedzamību. To vienmēr var atgriezt. Tiek aizvērti visi šī konta pārskati.
suspend_description_html: Konts un viss tā saturs nebūs pieejams un galu galā tiks izdzēsts, un mijiedarbība ar to nebūs iespējama. Atgriežams 30 dienu laikā. Tiek aizvērti visi šī konta pārskati.
actions_description_html: Izlem, kādas darbības jāveic, lai atrisinātu šo ziņojumu. Ja tiks pieņemti sodoši mēri pret kontu, par kuru ziņots, tam e-pastā tiks nosūtīts paziņojums, izņemot gadījumus, kad ir atlasīta kategorija <strong>Mēstules</strong>.
actions_description_remote_html: Izlem, kādas darbības jāveic, lai atrisinātu šo ziņojumu. Tas ietekmēs tikai to, kā <strong>tavs</strong> serveris sazinās ar šo attālo kontu un apstrādā tā saturu.
actions_no_posts: Šim ziņojumam nav saistītu ierakstu, kurus izdzēst
add_to_report: Pievienot varāk paziņošanai
are_you_sure: Vai esi pārliecināts?
assign_to_self: Piešķirt man
@ -1120,7 +1125,7 @@ lv:
confirm: Turpināt
hint_html: "<strong>Padoms:</strong> Nākamās stundas laikā mēs tev vairs neprasīsim paroli."
invalid_password: Nepareiza parole
prompt: Lai turpinātu, apstiprini paroli
prompt: Lai turpinātu, jāapstiprina parole
crypto:
errors:
invalid_key: nav derīga Ed25519 vai Curve25519 atslēga

View file

@ -24,6 +24,7 @@ pt-PT:
admin:
account_actions:
action: Executar acção
already_silenced: Esta conta já foi limitada.
already_suspended: Esta conta já foi suspensa.
title: Executar ação de moderação em %{acct}
account_moderation_notes:
@ -272,6 +273,7 @@ pt-PT:
reject_user_html: "%{name} rejeitou a inscrição de %{target}"
remove_avatar_user_html: "%{name} removeu a imagem de perfil de %{target}"
reopen_report_html: "%{name} reabriu a denúncia %{target}"
resend_user_html: "%{name} reenviou e-mail de confirmação para %{target}"
reset_password_user_html: "%{name} restabeleceu a palavra-passe do utilizador %{target}"
resolve_report_html: "%{name} resolveu a denúncia %{target}"
sensitive_account_html: "%{name} marcou a media de %{target} como sensível"
@ -432,6 +434,7 @@ pt-PT:
attempts_over_week:
one: "%{count} tentativa na última semana"
other: "%{count} tentativas de inscrição na última semana"
created_msg: Domínio de e-mail bloqueado com sucesso
delete: Eliminar
dns:
types:
@ -440,8 +443,12 @@ pt-PT:
new:
create: Adicionar domínio
resolve: Domínio de resolução
title: Bloquear novo domínio de e-mail
no_email_domain_block_selected: Não foram alterados quaisquer bloqueios de domínios de e-mail, uma vez que nenhum foi selecionado
not_permitted: Não permitido
resolved_dns_records_hint_html: O nome de domínio resolve para os seguintes domínios MX, que são, em última análise, responsáveis por aceitar o e-mail. Bloquear um domínio MX irá bloquear as inscrições de qualquer endereço de e-mail que use o mesmo domínio MX, mesmo quando o nome de domínio visível é diferente. <strong>Cuidado para não bloquear os principais provedores de e-mail.</strong>
resolved_through_html: Resolvido através de %{domain}
title: Domínios de e-mail bloqueados
export_domain_allows:
new:
title: Importar permissões de domínio
@ -595,7 +602,9 @@ pt-PT:
resolve_description_html: Nenhuma ação será tomada contra a conta denunciada, não será registada nenhuma reprimenda, e a denúncia será fechada.
silence_description_html: O perfil será visível apenas para aqueles que já o seguem ou o procurem manualmente, limitando fortemente o seu alcance. Pode sempre ser revertido. Encerrar todas as denúncias contra esta conta.
suspend_description_html: A conta e todo o seu conteúdo ficará inacessível e, eventualmente apagado, pelo que interagir com ela será impossível. Reversível durante 30 dias. Encerra todas as denúncias contra esta conta.
actions_description_html: Decida a ação a tomar para resolver esta denúncia. Se decidir por uma ação punitiva contra a conta denunciada, um e-mail de notificação será enviado, excetuando quando selecionada a categoria <strong>Spam</strong>.
actions_description_remote_html: Decida quais as medidas a tomar para resolver esta denúncia. Isso apenas afetará como <strong>o seu</strong> servido comunica com esta conta remota e gere o seu conteúdo.
actions_no_posts: Este relatório não tem nenhuma publicação associada para eliminar
add_to_report: Adicionar mais à denúncia
already_suspended_badges:
local: Já suspenso neste servidor
@ -659,6 +668,7 @@ pt-PT:
delete_data_html: Eliminar o perfil de <strong>@%{acct}</strong> e conteúdos daqui a 30 dias, a menos que entretanto sejam suspensos
preview_preamble_html: "<strong>@%{acct}</strong> receberá um aviso com o seguinte conteúdo:"
record_strike_html: Registar um ataque contra <strong>@%{acct}</strong> para ajudar a escalar futuras violações desta conta
send_email_html: Enviar um e-mail de aviso a <strong>@%{acct}</strong>
warning_placeholder: Argumentos adicionais opcionais para a acção de moderação.
target_origin: Origem da conta denunciada
title: Denúncias
@ -698,6 +708,7 @@ pt-PT:
manage_appeals: Gerir apelos
manage_appeals_description: Permite aos utilizadores rever recursos de moderação
manage_blocks: Gerir bloqueios
manage_blocks_description: Permite aos utilizadores bloquear fornecedores de e-mail e endereços IP
manage_custom_emojis: Gerir emojis personalizados
manage_custom_emojis_description: Permite aos utilizadores gerirem os emojis personalizados do servidor
manage_federation: Gerir federação
@ -715,6 +726,7 @@ pt-PT:
manage_taxonomies: Gerir taxonomias
manage_taxonomies_description: Permite aos utilizadores rever o conteúdo em tendência e atualizar as configurações de hashtag
manage_user_access: Gerir acesso de utilizador
manage_user_access_description: Permite aos utilizadores desativar a autenticação de dois factores de outros utilizadores, alterar o seu e-mail e reiniciar a sua palavra-passe
manage_users: Gerir utilizadores
manage_users_description: Permite aos utilizadores ver os detalhes de outros utilizadores e executar ações de moderação contra eles
manage_webhooks: Gerir webhooks
@ -789,6 +801,7 @@ pt-PT:
destroyed_msg: Envio do site eliminado com sucesso!
software_updates:
critical_update: Crítico — por favor, atualize rapidamente
description: Recomenda-se que mantenha a sua instalação do Mastodon atualizada para beneficiar das últimas correções e funcionalidades. Além disso, é por vezes crítico atualizar o Mastodon de forma atempada para evitar problemas de segurança. Por estas razões, o Mastodon verifica se há actualizações a cada 30 minutos e notifica-o de acordo com as suas preferências de notificação por e-mail.
documentation_link: Saber mais
release_notes: Notas de lançamento
title: Atualizações disponíveis
@ -888,6 +901,7 @@ pt-PT:
name: Nome
newest: Mais recente
oldest: Mais antiga
open: Visualizar Publicamente
reset: Repor
review: Estado da revisão
search: Pesquisar
@ -897,10 +911,16 @@ pt-PT:
trends:
allow: Permitir
approved: Aprovado
confirm_allow: Tem a certeza que pretende permitir as etiquetas selecionadas?
confirm_disallow: Tem a certeza que pretende rejeitar as etiquetas selecionadas?
disallow: Não permitir
links:
allow: Permitir hiperligação
allow_provider: Permitir editor
confirm_allow: Tem a certeza que pretende permitir as hiperligações selecionadas?
confirm_allow_provider: Tem a certeza que pretende permitir os fornecedores selecionados?
confirm_disallow: Tem a certeza que pretende rejeitar as hiperligações selecionadas?
confirm_disallow_provider: Tem a certeza que pretende rejeitar os fornecedores selecionados?
description_html: Estas são as ligações que presentemente estão a ser muito partilhadas por contas visíveis pelo seu servidor. Estas podem ajudar os seus utilizador a descobrir o que está a acontecer no mundo. Nenhuma ligação é exibida publicamente até que o editor a aprove. Também pode permitir ou rejeitar ligações em avulso.
disallow: Não permitir ligação
disallow_provider: Não permitir editor
@ -924,6 +944,10 @@ pt-PT:
statuses:
allow: Permitir publicação
allow_account: Permitir autor
confirm_allow: Tem a certeza que pretende aceitar os estados selecionados?
confirm_allow_account: Tem a certeza que pretende aceitar as contas selecionadas?
confirm_disallow: Tem a certeza que pretende rejeitar os estados selecionados?
confirm_disallow_account: Tem a certeza que pretende rejeitar as contas selecionadas?
description_html: Estas são publicações que o seu servidor conhece e que atualmente estão a ser frequentemente partilhadas e adicionadas aos favoritos. Isto pode ajudar os seus utilizadores, novos e retornados, a encontrar mais pessoas para seguir. Nenhuma publicação será exibida publicamente até que aprove o autor, e o autor permita que a sua conta seja sugerida a outros. Você também pode permitir ou rejeitar publicações individualmente.
disallow: Não permitir publicação
disallow_account: Não permitir autor
@ -1041,7 +1065,9 @@ pt-PT:
guide_link_text: Todos podem contribuir.
sensitive_content: Conteúdo problemático
application_mailer:
notification_preferences: Alterar preferências de e-mail
salutation: "%{name},"
settings: 'Alterar preferências de e-mail: %{link}'
unsubscribe: Cancelar subscrição
view: 'Ver:'
view_profile: Ver perfil
@ -1061,6 +1087,7 @@ pt-PT:
hint_html: Só mais uma coisa! Precisamos confirmar que você é um humano (isto para que possamos evitar spam!). Resolva o CAPTCHA abaixo e clique em "Continuar".
title: Verificação de segurança
confirmations:
awaiting_review: O seu endereço de e-mail está confirmado! A equipa de %{domain} está agora a analisar a sua inscrição. Receberá um e-mail se a sua conta for aprovada!
awaiting_review_title: A sua inscrição está a ser revista
clicking_this_link: clicar nesta hiperligação
login_link: iniciar sessão
@ -1068,6 +1095,7 @@ pt-PT:
redirect_to_app_html: Devia ter sido reencaminhado para a aplicação <strong>%{app_name}</strong>. Se isso não aconteceu, tente %{clicking_this_link} ou volte manualmente para a aplicação.
registration_complete: O seu registo sem %{domain} está agora concluído!
welcome_title: Bem-vindo(a), %{name}!
wrong_email_hint: Se este endereço de correio eletrónico não estiver correto, pode alterá-lo nas definições de conta.
delete_account: Eliminar conta
delete_account_html: Se deseja eliminar a sua conta, pode <a href="%{path}">continuar aqui</a>. Uma confirmação será solicitada.
description:
@ -1088,6 +1116,7 @@ pt-PT:
or_log_in_with: Ou iniciar sessão com
privacy_policy_agreement_html: Eu li e concordo com a <a href="%{privacy_policy_path}" target="_blank">política de privacidade</a>
progress:
confirm: Confirmar e-mail
details: Os seus dados
review: A nossa avaliação
rules: Aceitar regras
@ -1109,8 +1138,10 @@ pt-PT:
security: Alterar palavra-passe
set_new_password: Editar palavra-passe
setup:
email_below_hint_html: Verifique a sua pasta de spam ou solicite outra. Pode corrigir o seu endereço de e-mail se estiver errado.
email_settings_hint_html: Clique no link que enviamos para verificar %{email}. Esperaremos aqui.
link_not_received: Não recebeu um link?
new_confirmation_instructions_sent: Irá receber uma nova mensagem de e-mail com a ligação de confirmação dentro de alguns minutos!
title: Verifique a caixa de entrada do seu e-mail
sign_in:
preamble_html: Iniciar sessão com as suas credenciais de <strong>%{domain}</strong>. Se a sua conta estiver hospedada noutro servidor, não poderá iniciar sessão aqui.
@ -1121,12 +1152,20 @@ pt-PT:
title: Vamos lá inscrevê-lo em %{domain}.
status:
account_status: Estado da conta
confirming: A aguardar a confirmação do e-mail para ser concluída.
functional: A sua conta está totalmente operacional.
pending: A sua inscrição está a ser analisada pela nossa equipa. Este processo pode demorar algum tempo. Receberá um e-mail se a sua inscrição for aprovada.
redirecting_to: A sua conta está inativa porque está atualmente a ser redirecionada para %{acct}.
self_destruct: Como %{domain} vai fechar, só terá acesso limitado à sua conta.
view_strikes: Veja as reprimendas anteriores sobre a sua conta
too_fast: Formulário enviado demasiado rapidamente, tente novamente.
use_security_key: Usar chave de segurança
author_attribution:
example_title: Texto de exemplo
hint_html: Controle a forma como é creditado quando as hiperligações são partilhadas no Mastodon.
more_from_html: Mais de %{name}
s_blog: Blog de %{name}
title: Atribuição de autor
challenge:
confirm: Continuar
hint_html: "<strong>Dica:</strong> Não vamos pedir novamente a sua palavra-passe durante a próxima hora."
@ -1164,6 +1203,9 @@ pt-PT:
before: 'Antes de continuar, por favor leia cuidadosamente estas notas:'
caches: O conteúdo que foi armazenado em cache por outras instâncias pode perdurar
data_removal: As suas publicações e outros dados serão eliminados permanentemente
email_change_html: Pode <a href="%{path}">alterar o seu e-mail</a> sem eliminar a sua conta
email_contact_html: Se ainda assim não o recebeu, pode enviar um e-mail para <a href="mailto:%{email}">%{email}</a> para obter ajuda
email_reconfirmation_html: Se não recebeu a mensagem de e-mail de confirmação, pode <a href="%{path}">solicitá-la novamente</a>
irreversible: Não será possível restaurar ou reativar sua conta
more_details_html: Para mais pormenores, leia a <a href="%{terms_path}">política de privacidade</a>.
username_available: O seu nome de utilizador ficará novamente disponível
@ -1394,6 +1436,7 @@ pt-PT:
authentication_methods:
otp: aplicação de autenticação em duas etapas
password: palavra-passe
sign_in_token: código de segurança de e-mail
webauthn: chaves de segurança
description_html: Se vê atividade que não reconhece, considere alterar a sua palavra-passe e ativar a autenticação em duas etapas.
empty: Sem histórico de autenticação disponível

View file

@ -3,6 +3,7 @@ et:
simple_form:
hints:
account:
attribution_domains_as_text: Kaitseb valede omistuste eest.
discoverable: Su profiili ja avalikke postitusi võidakse Mastodoni erinevates piirkondades esile tõsta või soovitada ning su profiili soovitada teistele kasutajatele.
display_name: Su täisnimi või naljanimi.
fields: Su koduleht, sugu, vanus. Mistahes, mida soovid.
@ -143,6 +144,7 @@ et:
url: Kuhu sündmused saadetakse
labels:
account:
attribution_domains_as_text: Luba vaid kindlad veebilehed
discoverable: Tõsta postitused ja profiil avastamise algoritmides esile
fields:
name: Nimetus

View file

@ -78,9 +78,11 @@ fr-CA:
warn: Cacher le contenu filtré derrière un avertissement mentionnant le nom du filtre
form_admin_settings:
activity_api_enabled: Nombre de messages publiés localement, de comptes actifs et de nouvelles inscriptions par tranche hebdomadaire
app_icon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
backups_retention_period: Les utilisateur·rice·s ont la possibilité de générer des archives de leurs messages pour les télécharger plus tard. Lorsqu'elles sont définies à une valeur positive, ces archives seront automatiquement supprimées de votre stockage après le nombre de jours spécifié.
bootstrap_timeline_accounts: Ces comptes seront épinglés en tête de liste des recommandations pour les nouveaux utilisateurs.
closed_registrations_message: Affiché lorsque les inscriptions sont fermées
content_cache_retention_period: Tous les messages provenant d'autres serveurs (y compris les partages et les réponses) seront supprimés passé le nombre de jours spécifié, sans tenir compte de l'interaction de l'utilisateur·rice local·e avec ces messages. Cela inclut les messages qu'un·e utilisateur·rice aurait marqué comme signets ou comme favoris. Les mentions privées entre utilisateur·rice·s de différentes instances seront également perdues et impossibles à restaurer. L'utilisation de ce paramètre est destinée à des instances spécifiques et contrevient à de nombreuses attentes des utilisateurs lorsqu'elle est appliquée à des fins d'utilisation ordinaires.
custom_css: Vous pouvez appliquer des styles personnalisés sur la version Web de Mastodon.
favicon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
mascot: Remplace l'illustration dans l'interface Web avancée.

View file

@ -78,9 +78,11 @@ fr:
warn: Cacher le contenu filtré derrière un avertissement mentionnant le nom du filtre
form_admin_settings:
activity_api_enabled: Nombre de messages publiés localement, de comptes actifs et de nouvelles inscriptions par tranche hebdomadaire
app_icon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
backups_retention_period: Les utilisateur·rice·s ont la possibilité de générer des archives de leurs messages pour les télécharger plus tard. Lorsqu'elles sont définies à une valeur positive, ces archives seront automatiquement supprimées de votre stockage après le nombre de jours spécifié.
bootstrap_timeline_accounts: Ces comptes seront épinglés en tête de liste des recommandations pour les nouveaux utilisateurs.
closed_registrations_message: Affiché lorsque les inscriptions sont fermées
content_cache_retention_period: Tous les messages provenant d'autres serveurs (y compris les partages et les réponses) seront supprimés passé le nombre de jours spécifié, sans tenir compte de l'interaction de l'utilisateur·rice local·e avec ces messages. Cela inclut les messages qu'un·e utilisateur·rice aurait marqué comme signets ou comme favoris. Les mentions privées entre utilisateur·rice·s de différentes instances seront également perdues et impossibles à restaurer. L'utilisation de ce paramètre est destinée à des instances spécifiques et contrevient à de nombreuses attentes des utilisateurs lorsqu'elle est appliquée à des fins d'utilisation ordinaires.
custom_css: Vous pouvez appliquer des styles personnalisés sur la version Web de Mastodon.
favicon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
mascot: Remplace l'illustration dans l'interface Web avancée.

View file

@ -3,6 +3,7 @@ ga:
simple_form:
hints:
account:
attribution_domains_as_text: Cosnaíonn sé ó sannadh bréagach.
discoverable: Seans go mbeidh do phostálacha poiblí agus do phróifíl le feiceáil nó molta i réimsí éagsúla de Mastodon agus is féidir do phróifíl a mholadh dúsáideoirí eile.
display_name: D'ainm iomlán nó d'ainm spraoi.
fields: Do leathanach baile, forainmneacha, aois, rud ar bith is mian leat.
@ -143,6 +144,7 @@ ga:
url: An áit a seolfar imeachtaí chuig
labels:
account:
attribution_domains_as_text: Ná ceadaigh ach láithreáin ghréasáin ar leith
discoverable: Próifíl gné agus postálacha in halgartaim fionnachtana
fields:
name: Lipéad

View file

@ -3,6 +3,7 @@ gd:
simple_form:
hints:
account:
attribution_domains_as_text: Dìonadh seo o bhuaidh-aithrisean cearra.
discoverable: Dhfhaoidte gun dèid na postaichean poblach s a phròifil agad a bhrosnachadh no a mholadh ann an caochladh roinnean de Mhastodon agus gun dèid a phròifil agad a mholadh do chàch.
display_name: D ainm slàn no spòrsail.
fields: An duilleag-dhachaigh agad, roimhearan, aois, rud sam bith a thogras tu.
@ -143,6 +144,7 @@ gd:
url: Far an dèid na tachartasan a chur
labels:
account:
attribution_domains_as_text: Na ceadaich ach làraichean-lìnn sònraichte
discoverable: Brosnaich a phròifil is postaichean agad sna h-algairimean rùrachaidh
fields:
name: Leubail

View file

@ -90,6 +90,8 @@ lt:
timeline_preview: Atsijungę lankytojai galės naršyti naujausius viešus įrašus, esančius serveryje.
trends: Trendai rodo, kurios įrašai, saitažodžiai ir naujienų istorijos tavo serveryje sulaukia didžiausio susidomėjimo.
trends_as_landing_page: Rodyti tendencingą turinį atsijungusiems naudotojams ir lankytojams vietoj šio serverio aprašymo. Reikia, kad tendencijos būtų įjungtos.
imports:
data: CSV failas, eksportuotas iš kito „Mastodon“ serverio.
invite_request:
text: Tai padės mums peržiūrėti tavo paraišką
rule:

View file

@ -3,6 +3,7 @@ lv:
simple_form:
hints:
account:
attribution_domains_as_text: Aizsargā no nepatiesas piedēvēšanas.
discoverable: Tavas publiskās ziņas un profils var tikt piedāvāti vai ieteikti dažādās Mastodon vietās, un tavs profils var tikt ieteikts citiem lietotājiem.
display_name: Tavs pilnais vārds vai tavs joku vārds.
fields: Tava mājas lapa, vietniekvārdi, vecums, viss, ko vēlies.
@ -128,6 +129,7 @@ lv:
name: Tu vari mainīt tikai burtu lielumu, piemēram, lai tie būtu vieglāk lasāmi
user:
chosen_languages: Ja ieķeksēts, publiskos laika grafikos tiks parādītas tikai ziņas noteiktajās valodās
role: Loma nosaka, kādas lietotājam ir atļaujas.
user_role:
color: Krāsa, kas jāizmanto lomai visā lietotāja saskarnē, kā RGB hex formātā
highlighted: Tas padara lomu publiski redzamu
@ -140,6 +142,7 @@ lv:
url: Kur notikumi tiks nosūtīti
labels:
account:
attribution_domains_as_text: Ļaut tikai noteiktas tīmekļvietnes
discoverable: Funkcijas profils un ziņas atklāšanas algoritmos
fields:
name: Marķējums
@ -208,6 +211,7 @@ lv:
setting_default_privacy: Publicēšanas privātums
setting_default_sensitive: Atļaut atzīmēt multividi kā sensitīvu
setting_delete_modal: Parādīt apstiprinājuma dialogu pirms ziņas dzēšanas
setting_disable_hover_cards: Atspējot profila priekšskatījumu pēc kursora novietošanas
setting_disable_swiping: Atspējot vilkšanas kustības
setting_display_media: Multivides rādīšana
setting_display_media_default: Noklusējums
@ -239,11 +243,13 @@ lv:
warn: Paslēpt ar brīdinājumu
form_admin_settings:
activity_api_enabled: Publicējiet apkopotu statistiku par lietotāju darbībām API
app_icon: Lietotnes ikona
backups_retention_period: Lietotāja arhīva glabāšanas periods
bootstrap_timeline_accounts: Vienmēr iesaki šos kontus jaunajiem lietotājiem
closed_registrations_message: Pielāgots ziņojums, ja reģistrēšanās nav pieejama
content_cache_retention_period: Attālā satura paturēšanas laika posms
custom_css: Pielāgots CSS
favicon: Favikona
mascot: Pielāgots talismans (mantots)
media_cache_retention_period: Multivides kešatmiņas saglabāšanas periods
peers_api_enabled: Publicēt API atklāto serveru sarakstu
@ -308,6 +314,7 @@ lv:
listable: Atļaut šim tēmturim parādīties meklējumos un ieteikumos
name: Tēmturis
trendable: Atļaut šim tēmturim parādīties zem tendencēm
usable: Ļaut ierakstos vietēji izmantot šo tēmturi
user:
role: Loma
time_zone: Laika josla

View file

@ -3,6 +3,7 @@ th:
simple_form:
hints:
account:
attribution_domains_as_text: ปกป้องจากการระบุแหล่งที่มาที่ผิด
discoverable: อาจแสดงหรือแนะนำโพสต์และโปรไฟล์สาธารณะของคุณในพื้นที่ต่าง ๆ ของ Mastodon และอาจเสนอแนะโปรไฟล์ของคุณให้กับผู้ใช้อื่น ๆ
display_name: ชื่อเต็มของคุณหรือชื่อแบบสนุกสนานของคุณ
fields: หน้าแรก, สรรพนาม, อายุของคุณ สิ่งใดก็ตามที่คุณต้องการ
@ -143,6 +144,7 @@ th:
url: ที่ซึ่งจะส่งเหตุการณ์ไปยัง
labels:
account:
attribution_domains_as_text: อนุญาตเฉพาะเว็บไซต์ที่เฉพาะเจาะจงเท่านั้น
discoverable: แสดงโปรไฟล์และโพสต์ในอัลกอริทึมการค้นพบ
fields:
name: ป้ายชื่อ

View file

@ -3,6 +3,7 @@ vi:
simple_form:
hints:
account:
attribution_domains_as_text: Bảo vệ khỏi những sự gán ghép sai.
discoverable: Các tút và hồ sơ công khai của bạn có thể được giới thiệu hoặc đề xuất ở nhiều khu vực khác nhau của Mastodon và hồ sơ của bạn có thể được đề xuất cho những người dùng khác.
display_name: Tên đầy đủ hoặc biệt danh đều được.
fields: Trang blog của bạn, nghề nghiệp, tuổi hoặc bất cứ thứ gì.
@ -143,6 +144,7 @@ vi:
url: Nơi những sự kiện được gửi đến
labels:
account:
attribution_domains_as_text: Chỉ cho phép các website đặc biệt
discoverable: Cho phép khám phá hồ sơ
fields:
name: Nhãn

View file

@ -22,6 +22,7 @@ th:
admin:
account_actions:
action: ทำการกระทำ
already_silenced: มีการจำกัดบัญชีนี้ไปแล้ว
already_suspended: มีการระงับบัญชีนี้ไปแล้ว
title: ทำการกระทำการกลั่นกรองต่อ %{acct}
account_moderation_notes:
@ -1141,6 +1142,12 @@ th:
view_strikes: ดูการดำเนินการที่ผ่านมาต่อบัญชีของคุณ
too_fast: ส่งแบบฟอร์มเร็วเกินไป ลองอีกครั้ง
use_security_key: ใช้กุญแจความปลอดภัย
author_attribution:
example_title: ข้อความตัวอย่าง
hint_html: ควบคุมวิธีที่ให้เครดิตแก่คุณเมื่อมีการแบ่งปันลิงก์ใน Mastodon
more_from_html: เพิ่มเติมจาก %{name}
s_blog: บล็อกของ %{name}
title: การระบุแหล่งที่มาผู้สร้าง
challenge:
confirm: ดำเนินการต่อ
hint_html: "<strong>เคล็ดลับ:</strong> เราจะไม่ถามรหัสผ่านของคุณกับคุณสำหรับชั่วโมงถัดไป"
@ -1905,6 +1912,7 @@ th:
instructions_html: คัดลอกแล้ววางโค้ดด้านล่างลงใน HTML ของเว็บไซต์ของคุณ จากนั้นเพิ่มที่อยู่ของเว็บไซต์ของคุณลงในหนึ่งในช่องพิเศษในโปรไฟล์ของคุณจากแท็บ "แก้ไขโปรไฟล์" และบันทึกการเปลี่ยนแปลง
verification: การตรวจสอบ
verified_links: ลิงก์ที่ยืนยันแล้วของคุณ
website_verification: การตรวจสอบเว็บไซต์
webauthn_credentials:
add: เพิ่มกุญแจความปลอดภัยใหม่
create:

View file

@ -22,6 +22,7 @@ vi:
admin:
account_actions:
action: Thực hiện hành động
already_silenced: Tài khoản này đã bị hạn chế.
already_suspended: Tài khoản này đã bị vô hiệu hóa.
title: Áp đặt kiểm duyệt với %{acct}
account_moderation_notes:
@ -1141,6 +1142,12 @@ vi:
view_strikes: Xem những lần cảnh cáo cũ
too_fast: Nghi vấn đăng ký spam, xin thử lại.
use_security_key: Dùng khóa bảo mật
author_attribution:
example_title: Văn bản mẫu
hint_html: Kiểm soát cách bạn được ghi nhận khi chia sẻ liên kết trên Mastodon.
more_from_html: Thêm từ %{name}
s_blog: "%{name}'s Blog"
title: Ghi nhận tác giả
challenge:
confirm: Tiếp tục
hint_html: "<strong>Mẹo:</strong> Chúng tôi sẽ không hỏi lại mật khẩu của bạn sau này."
@ -1905,6 +1912,7 @@ vi:
instructions_html: Sao chép và dán mã bên dưới vào mã nguồn trang web của bạn. Sau đó, thêm địa chỉ trang web của bạn vào một trong các trường metadata trên hồ sơ của bạn từ tab "Sửa hồ sơ" và lưu thay đổi.
verification: Xác minh
verified_links: Những liên kết bạn đã xác minh
website_verification: Xác minh website
webauthn_credentials:
add: Thêm khóa bảo mật mới
create:

View file

@ -318,6 +318,21 @@ namespace :api, format: false do
end
end
concern :grouped_notifications do
resources :notifications, param: :group_key, only: [:index, :show] do
collection do
post :clear
get :unread_count
end
member do
post :dismiss
end
resources :accounts, only: [:index], module: :notifications
end
end
namespace :v2 do
get '/search', to: 'search#index', as: :search
@ -343,21 +358,12 @@ namespace :api, format: false do
namespace :notifications do
resource :policy, only: [:show, :update]
end
concerns :grouped_notifications
end
namespace :v2_alpha do
resources :notifications, param: :group_key, only: [:index, :show] do
collection do
post :clear
get :unread_count
end
member do
post :dismiss
end
resources :accounts, only: [:index], module: :notifications
end
namespace :v2_alpha, module: 'v2' do
concerns :grouped_notifications
end
namespace :web do

View file

@ -43,6 +43,12 @@ module Mastodon
@gem_version ||= Gem::Version.new(to_s.split('+')[0])
end
def api_versions
{
mastodon: 2,
}
end
def repository
ENV.fetch('GITHUB_REPOSITORY', 'glitch-soc/mastodon')
end

View file

@ -2,6 +2,8 @@
Fabricator(:list_account) do
list
account
before_create { |list_account, _| list_account.list.account.follow!(account) }
initialize_with do
resolved_class.new(list: list, account: list.account)
end
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Bookmark do
describe 'Associations' do
it { is_expected.to belong_to(:account).required }
it { is_expected.to belong_to(:status).required }
end
describe 'Validations' do
subject { Fabricate.build :bookmark }
it { is_expected.to validate_uniqueness_of(:status_id).scoped_to(:account_id) }
end
describe 'Callbacks' do
describe 'reblog statuses' do
context 'when status is not a reblog' do
let(:status) { Fabricate :status }
it 'keeps status set to assigned value' do
bookmark = Fabricate.build :bookmark, status: status
expect { bookmark.valid? }
.to_not change(bookmark, :status)
end
end
context 'when status is a reblog' do
let(:original) { Fabricate :status }
let(:status) { Fabricate :status, reblog: original }
it 'keeps status set to assigned value' do
bookmark = Fabricate.build :bookmark, status: status
expect { bookmark.valid? }
.to change(bookmark, :status).to(original)
end
end
end
end
end

View file

@ -84,4 +84,13 @@ RSpec.describe CustomEmoji, :attachment_processing do
it { is_expected.to normalize(:domain).from(nil).to(nil) }
end
end
describe 'Validations' do
subject { Fabricate.build :custom_emoji }
it { is_expected.to validate_uniqueness_of(:shortcode).scoped_to(:domain) }
it { is_expected.to validate_length_of(:shortcode).is_at_least(described_class::MINIMUM_SHORTCODE_SIZE) }
it { is_expected.to allow_values('cats').for(:shortcode) }
it { is_expected.to_not allow_values('@#$@#$', 'X').for(:shortcode) }
end
end

View file

@ -9,7 +9,7 @@ RSpec.describe ListAccount do
let(:list) { Fabricate :list, account: follow.account }
it 'finds and sets the follow with the list account' do
list_account = described_class.create list: list, account: follow.target_account
list_account = Fabricate :list_account, list: list, account: follow.target_account
expect(list_account)
.to have_attributes(
follow: eq(follow),
@ -23,7 +23,7 @@ RSpec.describe ListAccount do
let(:list) { Fabricate :list, account: follow_request.account }
it 'finds and sets the follow request with the list account' do
list_account = described_class.create list: list, account: follow_request.target_account
list_account = Fabricate :list_account, list: list, account: follow_request.target_account
expect(list_account)
.to have_attributes(
follow: be_nil,
@ -33,10 +33,8 @@ RSpec.describe ListAccount do
end
context 'when list owner is the account' do
let(:list) { Fabricate :list }
it 'does not set follow or follow request' do
list_account = described_class.create list: list, account: list.account
list_account = Fabricate :list_account
expect(list_account)
.to have_attributes(
follow: be_nil,

View file

@ -3,6 +3,12 @@
require 'rails_helper'
RSpec.describe PreviewCard do
describe 'file size limit', :attachment_processing do
it 'is set differently whether vips is enabled or not' do
expect(described_class::LIMIT).to eq(Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes)
end
end
describe 'validations' do
describe 'urls' do
it 'allows http schemes' do

View file

@ -8,9 +8,9 @@ RSpec.describe 'Accounts in grouped notifications' do
let(:scopes) { 'read:notifications write:notifications' }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
describe 'GET /api/v2_alpha/notifications/:group_key/accounts', :inline_jobs do
describe 'GET /api/v2/notifications/:group_key/accounts', :inline_jobs do
subject do
get "/api/v2_alpha/notifications/#{user.account.notifications.first.group_key}/accounts", headers: headers, params: params
get "/api/v2/notifications/#{user.account.notifications.first.group_key}/accounts", headers: headers, params: params
end
let(:params) { {} }
@ -71,8 +71,8 @@ RSpec.describe 'Accounts in grouped notifications' do
expect(response)
.to include_pagination_headers(
prev: api_v2_alpha_notification_accounts_url(limit: params[:limit], min_id: notifications.first.id),
next: api_v2_alpha_notification_accounts_url(limit: params[:limit], max_id: notifications.second.id)
prev: api_v2_notification_accounts_url(limit: params[:limit], min_id: notifications.first.id),
next: api_v2_notification_accounts_url(limit: params[:limit], max_id: notifications.second.id)
)
end
end

View file

@ -0,0 +1,343 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Notifications' do
let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'read:notifications write:notifications' }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
describe 'GET /api/v2/notifications/unread_count', :inline_jobs do
subject do
get '/api/v2/notifications/unread_count', headers: headers, params: params
end
let(:params) { {} }
before do
first_status = PostStatusService.new.call(user.account, text: 'Test')
ReblogService.new.call(Fabricate(:account), first_status)
PostStatusService.new.call(Fabricate(:account), text: 'Hello @alice')
FavouriteService.new.call(Fabricate(:account), first_status)
FavouriteService.new.call(Fabricate(:account), first_status)
FollowService.new.call(Fabricate(:account), user.account)
end
it_behaves_like 'forbidden for wrong scope', 'write write:notifications'
context 'with no options' do
it 'returns expected notifications count' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:count]).to eq 4
end
end
context 'with grouped_types parameter' do
let(:params) { { grouped_types: %w(reblog) } }
it 'returns expected notifications count' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:count]).to eq 5
end
end
context 'with a read marker' do
before do
id = user.account.notifications.browserable.order(id: :desc).offset(2).first.id
user.markers.create!(timeline: 'notifications', last_read_id: id)
end
it 'returns expected notifications count' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:count]).to eq 2
end
end
context 'with exclude_types param' do
let(:params) { { exclude_types: %w(mention) } }
it 'returns expected notifications count' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:count]).to eq 3
end
end
context 'with a user-provided limit' do
let(:params) { { limit: 2 } }
it 'returns a capped value' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:count]).to eq 2
end
end
context 'when there are more notifications than the limit' do
before do
stub_const('Api::V2::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT', 2)
end
it 'returns a capped value' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:count]).to eq Api::V2::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT
end
end
end
describe 'GET /api/v2/notifications', :inline_jobs do
subject do
get '/api/v2/notifications', headers: headers, params: params
end
let(:bob) { Fabricate(:user) }
let(:tom) { Fabricate(:user) }
let(:params) { {} }
before do
first_status = PostStatusService.new.call(user.account, text: 'Test')
ReblogService.new.call(bob.account, first_status)
PostStatusService.new.call(bob.account, text: 'Hello @alice')
FavouriteService.new.call(bob.account, first_status)
FavouriteService.new.call(tom.account, first_status)
FollowService.new.call(bob.account, user.account)
end
it_behaves_like 'forbidden for wrong scope', 'write write:notifications'
context 'when there are no notifications' do
before do
user.account.notifications.destroy_all
end
it 'returns 0 notifications' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:notification_groups]).to eq []
end
end
context 'with no options' do
it 'returns expected notification types', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(body_json_types).to include('reblog', 'mention', 'favourite', 'follow')
end
end
context 'with grouped_types param' do
let(:params) { { grouped_types: %w(reblog) } }
it 'returns everything, but does not group favourites' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:notification_groups]).to contain_exactly(
a_hash_including(
type: 'reblog',
sample_account_ids: [bob.account_id.to_s]
),
a_hash_including(
type: 'mention',
sample_account_ids: [bob.account_id.to_s]
),
a_hash_including(
type: 'favourite',
sample_account_ids: [bob.account_id.to_s]
),
a_hash_including(
type: 'favourite',
sample_account_ids: [tom.account_id.to_s]
),
a_hash_including(
type: 'follow',
sample_account_ids: [bob.account_id.to_s]
)
)
end
end
context 'with exclude_types param' do
let(:params) { { exclude_types: %w(mention) } }
it 'returns everything but excluded type', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body.size).to_not eq 0
expect(body_json_types.uniq).to_not include 'mention'
end
end
context 'with types param' do
let(:params) { { types: %w(mention) } }
it 'returns only requested type', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(body_json_types.uniq).to eq ['mention']
expect(response.parsed_body.dig(:notification_groups, 0, :page_min_id)).to_not be_nil
end
end
context 'with limit param' do
let(:params) { { limit: 3 } }
let(:notifications) { user.account.notifications.reorder(id: :desc) }
it 'returns the requested number of notifications paginated', :aggregate_failures do
subject
expect(response.parsed_body[:notification_groups].size)
.to eq(params[:limit])
expect(response)
.to include_pagination_headers(
prev: api_v2_notifications_url(limit: params[:limit], min_id: notifications.first.id),
# TODO: one downside of the current approach is that we return the first ID matching the group,
# not the last that has been skipped, so pagination is very likely to give overlap
next: api_v2_notifications_url(limit: params[:limit], max_id: notifications[3].id)
)
end
end
context 'with since_id param' do
let(:params) { { since_id: notifications[2].id } }
let(:notifications) { user.account.notifications.reorder(id: :desc) }
it 'returns the requested number of notifications paginated', :aggregate_failures do
subject
expect(response.parsed_body[:notification_groups].size)
.to eq(2)
expect(response)
.to include_pagination_headers(
prev: api_v2_notifications_url(limit: params[:limit], min_id: notifications.first.id),
# TODO: one downside of the current approach is that we return the first ID matching the group,
# not the last that has been skipped, so pagination is very likely to give overlap
next: api_v2_notifications_url(limit: params[:limit], max_id: notifications[1].id)
)
end
end
context 'when requesting stripped-down accounts' do
let(:params) { { expand_accounts: 'partial_avatars' } }
let(:recent_account) { Fabricate(:account) }
before do
FavouriteService.new.call(recent_account, user.account.statuses.first)
end
it 'returns an account in "partial_accounts", with the expected keys', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:partial_accounts].size).to be > 0
expect(response.parsed_body[:partial_accounts][0].keys.map(&:to_sym)).to contain_exactly(:acct, :avatar, :avatar_static, :bot, :id, :locked, :url)
expect(response.parsed_body[:partial_accounts].pluck(:id)).to_not include(recent_account.id.to_s)
expect(response.parsed_body[:accounts].pluck(:id)).to include(recent_account.id.to_s)
end
end
context 'when passing an invalid value for "expand_accounts"' do
let(:params) { { expand_accounts: 'unknown_foobar' } }
it 'returns http bad request' do
subject
expect(response).to have_http_status(400)
end
end
def body_json_types
response.parsed_body[:notification_groups].pluck(:type)
end
end
describe 'GET /api/v2/notifications/:id' do
subject do
get "/api/v2/notifications/#{notification.group_key}", headers: headers
end
let(:notification) { Fabricate(:notification, account: user.account, group_key: 'foobar') }
it_behaves_like 'forbidden for wrong scope', 'write write:notifications'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
context 'when notification belongs to someone else' do
let(:notification) { Fabricate(:notification, group_key: 'foobar') }
it 'returns http not found' do
subject
expect(response).to have_http_status(404)
end
end
end
describe 'POST /api/v2/notifications/:id/dismiss' do
subject do
post "/api/v2/notifications/#{notification.group_key}/dismiss", headers: headers
end
let!(:notification) { Fabricate(:notification, account: user.account, group_key: 'foobar') }
it_behaves_like 'forbidden for wrong scope', 'read read:notifications'
it 'destroys the notification' do
subject
expect(response).to have_http_status(200)
expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
context 'when notification belongs to someone else' do
let(:notification) { Fabricate(:notification) }
it 'returns http not found' do
subject
expect(response).to have_http_status(404)
end
end
end
describe 'POST /api/v2/notifications/clear' do
subject do
post '/api/v2/notifications/clear', headers: headers
end
before do
Fabricate(:notification, account: user.account)
end
it_behaves_like 'forbidden for wrong scope', 'read read:notifications'
it 'clears notifications for the account' do
subject
expect(user.account.reload.notifications).to be_empty
expect(response).to have_http_status(200)
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# TODO: remove this before 4.3.0-rc1
require 'rails_helper'
RSpec.describe 'Notifications' do
@ -84,14 +86,14 @@ RSpec.describe 'Notifications' do
context 'when there are more notifications than the limit' do
before do
stub_const('Api::V2Alpha::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT', 2)
stub_const('Api::V2::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT', 2)
end
it 'returns a capped value' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:count]).to eq Api::V2Alpha::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT
expect(response.parsed_body[:count]).to eq Api::V2::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT
end
end
end
@ -206,10 +208,10 @@ RSpec.describe 'Notifications' do
expect(response)
.to include_pagination_headers(
prev: api_v2_alpha_notifications_url(limit: params[:limit], min_id: notifications.first.id),
prev: api_v2_notifications_url(limit: params[:limit], min_id: notifications.first.id),
# TODO: one downside of the current approach is that we return the first ID matching the group,
# not the last that has been skipped, so pagination is very likely to give overlap
next: api_v2_alpha_notifications_url(limit: params[:limit], max_id: notifications[3].id)
next: api_v2_notifications_url(limit: params[:limit], max_id: notifications[3].id)
)
end
end
@ -226,10 +228,10 @@ RSpec.describe 'Notifications' do
expect(response)
.to include_pagination_headers(
prev: api_v2_alpha_notifications_url(limit: params[:limit], min_id: notifications.first.id),
prev: api_v2_notifications_url(limit: params[:limit], min_id: notifications.first.id),
# TODO: one downside of the current approach is that we return the first ID matching the group,
# not the last that has been skipped, so pagination is very likely to give overlap
next: api_v2_alpha_notifications_url(limit: params[:limit], max_id: notifications[1].id)
next: api_v2_notifications_url(limit: params[:limit], max_id: notifications[1].id)
)
end
end

View file

@ -3664,12 +3664,12 @@ __metadata:
linkType: hard
"@types/jest@npm:^29.5.2":
version: 29.5.12
resolution: "@types/jest@npm:29.5.12"
version: 29.5.13
resolution: "@types/jest@npm:29.5.13"
dependencies:
expect: "npm:^29.0.0"
pretty-format: "npm:^29.0.0"
checksum: 10c0/25fc8e4c611fa6c4421e631432e9f0a6865a8cb07c9815ec9ac90d630271cad773b2ee5fe08066f7b95bebd18bb967f8ce05d018ee9ab0430f9dfd1d84665b6f
checksum: 10c0/9c31af0b155387b9860908830de63c6b79011d7c87c8b61b39da124e26e55423dd51b006749aafe4f0ef3a065016619a1f93ef4b055157d43727f448e67824b7
languageName: node
linkType: hard
@ -3771,13 +3771,13 @@ __metadata:
linkType: hard
"@types/pg@npm:^8.6.6":
version: 8.11.8
resolution: "@types/pg@npm:8.11.8"
version: 8.11.10
resolution: "@types/pg@npm:8.11.10"
dependencies:
"@types/node": "npm:*"
pg-protocol: "npm:*"
pg-types: "npm:^4.0.1"
checksum: 10c0/040eb04edda338a13dccee47585b4479549fd54561e1bc3768690545adb8708a089b178e04fab9241935d7bad361314fc57af3ad87b391cfb9dc0895dd049763
checksum: 10c0/c8800d0ab2c6424308e6c6b40c73f19583ee1aed758462bd07694844b0a551b5841442205a4ee05207b80109ba502f33f20241b1bd9b4902e713611fb9e08f6c
languageName: node
linkType: hard
@ -9559,11 +9559,11 @@ __metadata:
linkType: hard
"husky@npm:^9.0.11":
version: 9.1.5
resolution: "husky@npm:9.1.5"
version: 9.1.6
resolution: "husky@npm:9.1.6"
bin:
husky: bin.js
checksum: 10c0/f42efb95a026303eb880898760f802d88409780dd72f17781d2dfc302177d4f80b641cf1f1694f53f6d97c536c7397684133d8c8fe4a4426f7460186a7d1c6b8
checksum: 10c0/705673db4a247c1febd9c5df5f6a3519106cf0335845027bb50a15fba9b1f542cb2610932ede96fd08008f6d9f49db0f15560509861808b0031cdc0e7c798bac
languageName: node
linkType: hard
@ -13112,10 +13112,10 @@ __metadata:
languageName: node
linkType: hard
"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1":
version: 1.0.1
resolution: "picocolors@npm:1.0.1"
checksum: 10c0/c63cdad2bf812ef0d66c8db29583802355d4ca67b9285d846f390cc15c2f6ccb94e8cb7eb6a6e97fc5990a6d3ad4ae42d86c84d3146e667c739a4234ed50d400
"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.0":
version: 1.1.0
resolution: "picocolors@npm:1.1.0"
checksum: 10c0/86946f6032148801ef09c051c6fb13b5cf942eaf147e30ea79edb91dd32d700934edebe782a1078ff859fb2b816792e97ef4dab03d7f0b804f6b01a0df35e023
languageName: node
linkType: hard
@ -14074,13 +14074,13 @@ __metadata:
linkType: hard
"postcss@npm:^8.2.15, postcss@npm:^8.4.24, postcss@npm:^8.4.41":
version: 8.4.45
resolution: "postcss@npm:8.4.45"
version: 8.4.47
resolution: "postcss@npm:8.4.47"
dependencies:
nanoid: "npm:^3.3.7"
picocolors: "npm:^1.0.1"
source-map-js: "npm:^1.2.0"
checksum: 10c0/ad6f8b9b1157d678560373696109745ab97a947d449f8a997acac41c7f1e4c0f3ca4b092d6df1387f430f2c9a319987b1780dbdc27e35800a88cde9b606c1e8f
picocolors: "npm:^1.1.0"
source-map-js: "npm:^1.2.1"
checksum: 10c0/929f68b5081b7202709456532cee2a145c1843d391508c5a09de2517e8c4791638f71dd63b1898dba6712f8839d7a6da046c72a5e44c162e908f5911f57b5f44
languageName: node
linkType: hard
@ -16005,10 +16005,10 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0":
version: 1.2.0
resolution: "source-map-js@npm:1.2.0"
checksum: 10c0/7e5f896ac10a3a50fe2898e5009c58ff0dc102dcb056ed27a354623a0ece8954d4b2649e1a1b2b52ef2e161d26f8859c7710350930751640e71e374fe2d321a4
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
languageName: node
linkType: hard