Merge pull request #3464 from glitch-soc/glitch-soc/merge-upstream

Merge upstream changes up to ca5c0a144ade4bff7bd10446a39d86116879884e
This commit is contained in:
Claire 2026-04-01 20:15:21 +02:00 committed by GitHub
commit 2bf97b3a47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
126 changed files with 1669 additions and 1507 deletions

View File

@ -1,46 +1,83 @@
# Contributor Covenant Code of Conduct # Contributor Covenant 3.0 Code of Conduct
## Our Pledge ## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to make our community welcoming, safe, and equitable for all.
## Our Standards We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant.
Examples of behavior that contributes to creating a positive environment include: ## Encouraged Behaviors
- Using welcoming and inclusive language While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language.
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include: With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
- The use of sexualized language or imagery and unwelcome sexual attention or advances 1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
- Trolling, insulting/derogatory comments, and personal or political attacks 2. Engaging **kindly and honestly** with others.
- Public or private harassment 3. Respecting **different viewpoints** and experiences.
- Publishing others' private information, such as a physical or electronic address, without explicit permission 4. **Taking responsibility** for our actions and contributions.
- Other conduct which could reasonably be considered inappropriate in a professional setting 5. Gracefully giving and accepting **constructive feedback**.
6. Committing to **repairing harm** when it occurs.
7. Behaving in other ways that promote and sustain the **well-being of our community**.
## Our Responsibilities ## Restricted Behaviors
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop.
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people.
3. **Stereotyping or discrimination.** Characterizing anyones personality or behavior on the basis of immutable identities or traits.
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community.
5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission.
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
7. Behaving in other ways that **threaten the well-being** of our community.
### Other Restrictions
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions.
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community.
4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors.
## Reporting an Issue
Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm.
When an incident does occur, it is important to report it promptly. To report a possible violation, send an email describing the situation to glitch-abuse@sitedethib.com.
Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution.
## Addressing and Repairing Harm
If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped.
1. Warning
1. Event: A violation involving a single incident or series of incidents.
2. Consequence: A private, written warning from the Community Moderators.
3. Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations.
2. Temporarily Limited Activities
1. Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation.
2. Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members.
3. Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over.
3. Temporary Suspension
1. Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation.
2. Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions.
3. Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted.
4. Permanent Ban
1. Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member.
2. Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior.
3. Repair: There is no possible repair in cases of this severity.
This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community.
## Scope ## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at glitch-abuse@sitedethib.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
[homepage]: https://contributor-covenant.org Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
[version]: https://contributor-covenant.org/version/1/4/
For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozillas code of conduct team](https://github.com/mozilla/inclusion).

View File

@ -8,6 +8,8 @@ class Api::V1::Accounts::EmailSubscriptionsController < Api::BaseController
def create def create
@account.email_subscriptions.create!(email: params[:email], locale: I18n.locale) @account.email_subscriptions.create!(email: params[:email], locale: I18n.locale)
render_empty render_empty
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e).as_json, status: 422
end end
private private

View File

@ -101,7 +101,7 @@ module SignatureVerification
end end
if key_id.start_with?('acct:') if key_id.start_with?('acct:')
stoplight_wrapper.run { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } stoplight_wrapper.run { fetch_key_from_acct(key_id.delete_prefix('acct:')) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id) elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
keypair = Keypair.from_keyid(key_id) keypair = Keypair.from_keyid(key_id)
return keypair if keypair.present? return keypair if keypair.present?
@ -114,6 +114,15 @@ module SignatureVerification
raise Mastodon::SignatureVerificationError, e.message raise Mastodon::SignatureVerificationError, e.message
end end
def fetch_key_from_acct(acct)
# This is legacy and can't let us pick a specific key, so pick the first
account = ResolveAccountService.new.call(acct, suppress_errors: false)
return if account.nil?
account.keypairs.first || Keypair.from_legacy_account(account)
end
def stoplight_wrapper def stoplight_wrapper
Stoplight( Stoplight(
"source:#{request.remote_ip}", "source:#{request.remote_ip}",

View File

@ -75,3 +75,6 @@ export const apiDeleteProfileAvatar = () =>
export const apiDeleteProfileHeader = () => export const apiDeleteProfileHeader = () =>
apiRequestDelete('v1/profile/header'); apiRequestDelete('v1/profile/header');
export const apiSubscribeByEmail = (id: string, email: string) =>
apiRequestPost(`v1/accounts/${id}/email_subscriptions`, { email });

View File

@ -68,6 +68,7 @@ export interface BaseApiAccountJSON {
limited?: boolean; limited?: boolean;
memorial?: boolean; memorial?: boolean;
hide_collections: boolean; hide_collections: boolean;
email_subscriptions?: boolean;
} }
// See app/serializers/rest/muted_account_serializer.rb // See app/serializers/rest/muted_account_serializer.rb

View File

@ -0,0 +1,22 @@
export type ErrorToken =
| 'ERR_TAKEN'
| 'ERR_INVALID'
| 'ERR_BLOCKED'
| 'ERR_RESERVED'
| 'ERR_TOO_MANY'
| 'ERR_MALFORMED'
| 'ERR_UNUSABLE'
| 'ERR_TOO_SOON'
| 'ERR_BELOW_LIMIT'
| 'ERR_UNREACHABLE'
| 'ERR_ELEVATED';
export interface ValidationError {
error: ErrorToken;
description: string;
}
export interface ValidationErrorResponse {
error: string;
details: Record<string, ValidationError[]>;
}

View File

@ -6,7 +6,6 @@ import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useIdentity } from '@/flavours/glitch/identity_context'; import { useIdentity } from '@/flavours/glitch/identity_context';
import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
import { import {
fetchRelationships, fetchRelationships,
followAccount, followAccount,
@ -171,23 +170,10 @@ export const FollowButton: React.FC<{
'button--compact': compact, 'button--compact': compact,
}); });
if (isServerFeatureEnabled('profile_redesign')) {
return (
<Link to='/profile/edit' className={buttonClasses}>
{label}
</Link>
);
}
return ( return (
<a <Link to='/profile/edit' className={buttonClasses}>
href='/settings/profile'
target='_blank'
rel='noopener'
className={buttonClasses}
>
{label} {label}
</a> </Link>
); );
} }

View File

@ -23,6 +23,7 @@
a { a {
padding: 0; padding: 0;
text-decoration: none;
&:hover, &:hover,
&:focus { &:focus {

View File

@ -398,7 +398,10 @@ class StatusActionBar extends ImmutablePureComponent {
<div className='status__action-bar-spacer' /> <div className='status__action-bar-spacer' />
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>} <RelativeTimestamp
timestamp={status.get('created_at')}
noFuture
/>{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a> </a>
</div> </div>
); );

View File

@ -210,7 +210,7 @@ export const EditFieldModal = forwardRef<
const labelStatus = checkField(newLabel); const labelStatus = checkField(newLabel);
const valueStatus = checkField(newValue); const valueStatus = checkField(newValue);
if (labelStatus || valueStatus) { if (labelStatus?.variant === 'error' || valueStatus?.variant === 'error') {
setFieldStatuses({ setFieldStatuses({
label: labelStatus ?? undefined, label: labelStatus ?? undefined,
value: valueStatus ?? undefined, value: valueStatus ?? undefined,

View File

@ -182,6 +182,11 @@
// Field component // Field component
.fieldName,
.fieldValue {
word-break: break-all;
}
.fieldName { .fieldName {
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 13px; font-size: 13px;

View File

@ -7,7 +7,6 @@ import { useHistory } from 'react-router';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
import { fetchEndorsedAccounts } from 'flavours/glitch/actions/accounts'; import { fetchEndorsedAccounts } from 'flavours/glitch/actions/accounts';
import { fetchFeaturedTags } from 'flavours/glitch/actions/featured_tags'; import { fetchFeaturedTags } from 'flavours/glitch/actions/featured_tags';
import { Account } from 'flavours/glitch/components/account'; import { Account } from 'flavours/glitch/components/account';
@ -49,11 +48,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
const history = useHistory(); const history = useHistory();
useEffect(() => { useEffect(() => {
if ( if (account && !account.show_featured) {
account &&
!account.show_featured &&
isServerFeatureEnabled('profile_redesign')
) {
history.push(`/@${account.acct}`); history.push(`/@${account.acct}`);
} }
}, [account, history]); }, [account, history]);
@ -111,8 +106,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
); );
} }
const noTags = const noTags = featuredTags.isEmpty();
featuredTags.isEmpty() || isServerFeatureEnabled('profile_redesign');
if ( if (
noTags && noTags &&

View File

@ -4,7 +4,6 @@ import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { List as ImmutableList, isList } from 'immutable'; import { List as ImmutableList, isList } from 'immutable';
import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
import PersonIcon from '@/material-icons/400-24px/person.svg?react'; import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
@ -31,8 +30,6 @@ const messages = defineMessages({
const emptyList = ImmutableList<MediaAttachment>(); const emptyList = ImmutableList<MediaAttachment>();
const redesignEnabled = isServerFeatureEnabled('profile_redesign');
const selectGalleryTimeline = createAppSelector( const selectGalleryTimeline = createAppSelector(
[ [
(_state, accountId?: string | null) => accountId, (_state, accountId?: string | null) => accountId,
@ -62,7 +59,7 @@ const selectGalleryTimeline = createAppSelector(
const { show_media, show_media_replies } = account; const { show_media, show_media_replies } = account;
// If the account disabled showing media, don't display anything. // If the account disabled showing media, don't display anything.
if (!show_media && redesignEnabled) { if (!show_media) {
return { return {
items, items,
hasMore: false, hasMore: false,
@ -71,7 +68,7 @@ const selectGalleryTimeline = createAppSelector(
}; };
} }
const withReplies = show_media_replies && redesignEnabled; const withReplies = show_media_replies;
const timeline = timelines.get( const timeline = timelines.get(
`account:${accountId}:media${withReplies ? ':with_replies' : ''}`, `account:${accountId}:media${withReplies ? ':with_replies' : ''}`,
); );
@ -233,7 +230,7 @@ export const AccountGallery: React.FC<{
alwaysPrepend alwaysPrepend
append={accountId && <RemoteHint accountId={accountId} />} append={accountId && <RemoteHint accountId={accountId} />}
scrollKey='account_gallery' scrollKey='account_gallery'
showLoading={isLoading} isLoading={isLoading}
hasMore={!forceEmptyState && hasMore} hasMore={!forceEmptyState && hasMore}
onLoadMore={handleLoadMore} onLoadMore={handleLoadMore}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

View File

@ -1,9 +1,4 @@
import type { AccountFieldShape } from '@/flavours/glitch/models/account'; import type { AccountFieldShape } from '@/flavours/glitch/models/account';
import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
export function isRedesignEnabled() {
return isServerFeatureEnabled('profile_redesign');
}
export interface AccountField extends AccountFieldShape { export interface AccountField extends AccountFieldShape {
nameHasEmojis: boolean; nameHasEmojis: boolean;

View File

@ -7,7 +7,6 @@ import { openModal } from '@/flavours/glitch/actions/modal';
import { AccountBio } from '@/flavours/glitch/components/account_bio'; import { AccountBio } from '@/flavours/glitch/components/account_bio';
import { Avatar } from '@/flavours/glitch/components/avatar'; import { Avatar } from '@/flavours/glitch/components/avatar';
import { AnimateEmojiProvider } from '@/flavours/glitch/components/emoji/context'; import { AnimateEmojiProvider } from '@/flavours/glitch/components/emoji/context';
import { AccountNote } from '@/flavours/glitch/features/account/components/account_note';
import FollowRequestNoteContainer from '@/flavours/glitch/features/account/containers/follow_request_note_container'; import FollowRequestNoteContainer from '@/flavours/glitch/features/account/containers/follow_request_note_container';
import { useLayout } from '@/flavours/glitch/hooks/useLayout'; import { useLayout } from '@/flavours/glitch/hooks/useLayout';
import { useVisibility } from '@/flavours/glitch/hooks/useVisibility'; import { useVisibility } from '@/flavours/glitch/hooks/useVisibility';
@ -20,10 +19,8 @@ import type { Account } from '@/flavours/glitch/models/account';
import { getAccountHidden } from '@/flavours/glitch/selectors/accounts'; import { getAccountHidden } from '@/flavours/glitch/selectors/accounts';
import { useAppSelector, useAppDispatch } from '@/flavours/glitch/store'; import { useAppSelector, useAppDispatch } from '@/flavours/glitch/store';
import { ActionBar } from '../../account/components/action_bar';
import { isRedesignEnabled } from '../common';
import { AccountName } from './account_name'; import { AccountName } from './account_name';
import { AccountSubscriptionForm } from './account_subscription_form';
import { AccountBadges } from './badges'; import { AccountBadges } from './badges';
import { AccountButtons } from './buttons'; import { AccountButtons } from './buttons';
import { FamiliarFollowers } from './familiar_followers'; import { FamiliarFollowers } from './familiar_followers';
@ -32,6 +29,7 @@ import { AccountInfo } from './info';
import { MemorialNote } from './memorial_note'; import { MemorialNote } from './memorial_note';
import { MovedNote } from './moved_note'; import { MovedNote } from './moved_note';
import { AccountNote as AccountNoteRedesign } from './note'; import { AccountNote as AccountNoteRedesign } from './note';
import { AccountNumberFields } from './number_fields';
import redesignClasses from './redesign.module.scss'; import redesignClasses from './redesign.module.scss';
import { AccountTabs } from './tabs'; import { AccountTabs } from './tabs';
@ -51,8 +49,6 @@ export const AccountHeader: React.FC<{
accountId: string; accountId: string;
hideTabs?: boolean; hideTabs?: boolean;
}> = ({ accountId, hideTabs }) => { }> = ({ accountId, hideTabs }) => {
const isRedesign = isRedesignEnabled();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const account = useAppSelector((state) => state.accounts.get(accountId)); const account = useAppSelector((state) => state.accounts.get(accountId));
const relationship = useAppSelector((state) => const relationship = useAppSelector((state) =>
@ -119,7 +115,7 @@ export const AccountHeader: React.FC<{
<div <div
className={classNames( className={classNames(
'account__header__image', 'account__header__image',
isRedesign && redesignClasses.header, redesignClasses.header,
)} )}
> >
{me !== account.id && relationship && ( {me !== account.id && relationship && (
@ -138,13 +134,13 @@ export const AccountHeader: React.FC<{
<div <div
className={classNames( className={classNames(
'account__header__bar', 'account__header__bar',
isRedesign && redesignClasses.barWrapper, redesignClasses.barWrapper,
)} )}
> >
<div <div
className={classNames( className={classNames(
'account__header__tabs', 'account__header__tabs',
isRedesign && redesignClasses.avatarWrapper, redesignClasses.avatarWrapper,
)} )}
> >
<a <a
@ -156,33 +152,24 @@ export const AccountHeader: React.FC<{
> >
<Avatar <Avatar
account={suspendedOrHidden ? undefined : account} account={suspendedOrHidden ? undefined : account}
size={isRedesign ? 80 : 92} size={80}
/> />
</a> </a>
{!isRedesign && (
<AccountButtons
accountId={accountId}
className='account__header__buttons--desktop'
/>
)}
</div> </div>
<div <div
className={classNames( className={classNames(
'account__header__tabs__name', 'account__header__tabs__name',
isRedesign && redesignClasses.nameWrapper, redesignClasses.nameWrapper,
)} )}
> >
<AccountName accountId={accountId} /> <AccountName accountId={accountId} />
{isRedesign && ( <AccountButtons
<AccountButtons accountId={accountId}
accountId={accountId} className={redesignClasses.buttonsDesktop}
className={redesignClasses.buttonsDesktop} noShare={!isMe || 'share' in navigator}
noShare={!isMe || 'share' in navigator} forceMenu={'share' in navigator}
forceMenu={'share' in navigator} />
/>
)}
</div> </div>
<AccountBadges accountId={accountId} /> <AccountBadges accountId={accountId} />
@ -191,54 +178,45 @@ export const AccountHeader: React.FC<{
<FamiliarFollowers accountId={accountId} /> <FamiliarFollowers accountId={accountId} />
)} )}
{!isRedesign && (
<AccountButtons
className='account__header__buttons--mobile'
accountId={accountId}
noShare
/>
)}
{!suspendedOrHidden && ( {!suspendedOrHidden && (
<div className='account__header__extra'> <div className='account__header__extra'>
<div className='account__header__bio'> <div className='account__header__bio'>
{me && {me && account.id !== me && (
account.id !== me && <AccountNoteRedesign accountId={accountId} />
(isRedesign ? ( )}
<AccountNoteRedesign accountId={accountId} />
) : (
<AccountNote accountId={accountId} />
))}
<AccountBio <AccountBio
showDropdown showDropdown
accountId={accountId} accountId={accountId}
className={classNames( className={classNames(
'account__header__content', 'account__header__content',
isRedesign && redesignClasses.bio, redesignClasses.bio,
)} )}
/> />
<AccountHeaderFields accountId={accountId} /> <AccountHeaderFields accountId={accountId} />
</div> </div>
{!me && account.email_subscriptions && (
<AccountSubscriptionForm accountId={accountId} />
)}
<AccountNumberFields accountId={accountId} />
</div> </div>
)} )}
{isRedesign && ( <AccountButtons
<AccountButtons className={classNames(
className={classNames( redesignClasses.buttonsMobile,
redesignClasses.buttonsMobile, !isIntersecting && redesignClasses.buttonsMobileIsStuck,
!isIntersecting && redesignClasses.buttonsMobileIsStuck, )}
)} accountId={accountId}
accountId={accountId} noShare
noShare />
/>
)}
</div> </div>
</AnimateEmojiProvider> </AnimateEmojiProvider>
<ActionBar account={account} /> {!hideTabs && !hidden && <AccountTabs />}
{!hideTabs && !hidden && <AccountTabs acct={account.acct} />}
<div ref={observedRef} /> <div ref={observedRef} />
<Helmet> <Helmet>

View File

@ -14,10 +14,6 @@ import { useAppSelector } from '@/flavours/glitch/store';
import AtIcon from '@/material-icons/400-24px/alternate_email.svg?react'; import AtIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import HelpIcon from '@/material-icons/400-24px/help.svg?react'; import HelpIcon from '@/material-icons/400-24px/help.svg?react';
import DomainIcon from '@/material-icons/400-24px/language.svg?react'; import DomainIcon from '@/material-icons/400-24px/language.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import { DomainPill } from '../../account/components/domain_pill';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
@ -34,7 +30,6 @@ const messages = defineMessages({
}); });
export const AccountName: FC<{ accountId: string }> = ({ accountId }) => { export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl();
const account = useAccount(accountId); const account = useAccount(accountId);
const me = useAppSelector((state) => state.meta.get('me') as string); const me = useAppSelector((state) => state.meta.get('me') as string);
const localDomain = useAppSelector( const localDomain = useAppSelector(
@ -47,32 +42,6 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
const [username = '', domain = localDomain] = account.acct.split('@'); const [username = '', domain = localDomain] = account.acct.split('@');
if (!isRedesignEnabled()) {
return (
<h1>
<DisplayName account={account} variant='simple' />
<small>
<span>
@{username}
<span className='invisible'>@{domain}</span>
</span>
<DomainPill
username={username}
domain={domain}
isSelf={me === account.id}
/>
{account.locked && (
<Icon
id='lock'
icon={LockIcon}
aria-label={intl.formatMessage(messages.lockedInfo)}
/>
)}
</small>
</h1>
);
}
return ( return (
<div className={classes.name}> <div className={classes.name}>
<h1> <h1>

View File

@ -0,0 +1,207 @@
import { useState, useCallback, useId } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import type { IntlShape } from 'react-intl';
import { Link } from 'react-router-dom';
import { AxiosError } from 'axios';
import { apiSubscribeByEmail } from 'flavours/glitch/api/accounts';
import type {
ValidationErrorResponse,
ValidationError,
} from 'flavours/glitch/api_types/errors';
import { A11yLiveRegion } from 'flavours/glitch/components/a11y_live_region';
import { Button } from 'flavours/glitch/components/button';
import { CalloutInline } from 'flavours/glitch/components/callout_inline';
import { DisplayName } from 'flavours/glitch/components/display_name';
import type { FieldStatus } from 'flavours/glitch/components/form_fields';
import formFieldClasses from 'flavours/glitch/components/form_fields/form_field_wrapper.module.scss';
import { TextInput } from 'flavours/glitch/components/form_fields/text_input_field';
import { useAppSelector } from 'flavours/glitch/store';
import classes from './redesign.module.scss';
const messages = defineMessages({
emailInvalid: {
id: 'email_subscriptions.validation.email.invalid',
defaultMessage: 'Invalid email address',
},
emailBlocked: {
id: 'email_subscriptions.validation.email.blocked',
defaultMessage: 'Blocked email provider',
},
email: {
id: 'email_subscriptions.email',
defaultMessage: 'Email address',
},
});
const isValidationErrorResponse = (
data: unknown,
): data is ValidationErrorResponse =>
typeof data === 'object' &&
data !== null &&
'error' in data &&
'details' in data;
const fieldStatusFromErrors = (
intl: IntlShape,
errors: ValidationError[],
): FieldStatus | undefined => {
const error = errors[0];
if (!error) {
return undefined;
}
let message: string;
switch (error.error) {
case 'ERR_BLOCKED':
message = intl.formatMessage(messages.emailBlocked);
break;
case 'ERR_INVALID':
default:
message = intl.formatMessage(messages.emailInvalid);
break;
}
return { variant: 'error', message };
};
export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
accountId,
}) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
const intl = useIntl();
const accessibilityId = useId();
const [email, setEmail] = useState('');
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [errors, setErrors] = useState<Record<string, ValidationError[]>>({});
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(e) => {
setEmail(e.target.value);
setErrors({});
},
[],
);
const handleSubmit = useCallback<React.FormEventHandler>(
(e) => {
e.preventDefault();
if (email.length === 0) {
return;
}
setSubmitting(true);
apiSubscribeByEmail(accountId, email)
.then(() => {
setSubmitting(false);
setSubmitted(true);
return '';
})
.catch((err: unknown) => {
setSubmitting(false);
if (err instanceof AxiosError && err.response) {
const data: unknown = err.response.data;
if (isValidationErrorResponse(data)) {
if (data.details.email?.some((k) => k.error === 'ERR_TAKEN')) {
setSubmitted(true);
return;
}
setErrors(data.details);
}
}
});
},
[accountId, email],
);
if (submitted) {
return (
<div className={classes.bannerBaseCentered}>
<div className={classes.bannerTextAndActions}>
<h2>
<FormattedMessage
id='email_subscriptions.submitted.title'
defaultMessage='One more step'
/>
</h2>
<FormattedMessage
id='email_subscriptions.submitted.lead'
defaultMessage='Check your inbox for an email to finish signing up for email updates.'
/>
</div>
</div>
);
}
return (
<form onSubmit={handleSubmit} className={classes.bannerBase} noValidate>
<div className={classes.bannerTextAndActions}>
<h2>
<FormattedMessage
id='email_subscriptions.form.title'
defaultMessage='Sign up for email updates from {name}'
values={{
name: <DisplayName account={account} variant='simple' />,
}}
/>
</h2>
<FormattedMessage
id='email_subscriptions.form.lead'
defaultMessage='Get posts in your inbox without creating a Mastodon account.'
/>
</div>
<div className={classes.bannerInputButton}>
<div className={formFieldClasses.wrapper}>
<TextInput
id={`${accessibilityId}-input`}
type='email'
value={email}
onChange={handleChange}
placeholder='name@email.com'
aria-label={intl.formatMessage(messages.email)}
aria-describedby={errors.email ? `${accessibilityId}-status` : ''}
/>
<A11yLiveRegion
className={formFieldClasses.status}
id={`${accessibilityId}-status`}
>
{errors.email && (
<CalloutInline {...fieldStatusFromErrors(intl, errors.email)} />
)}
</A11yLiveRegion>
</div>
<Button type='submit' loading={submitting}>
<FormattedMessage
id='email_subscriptions.form.action'
defaultMessage='Subscribe'
/>
</Button>
</div>
<div className={classes.bannerDisclaimer}>
<FormattedMessage
id='email_subscriptions.form.disclaimer'
defaultMessage='You can unsubscribe at any time. For more information, refer to the <a>Privacy Policy</a>.'
values={{ a: (str) => <Link to='/privacy-policy'>{str}</Link> }}
/>
</div>
</form>
);
};

View File

@ -20,8 +20,6 @@ import type { AccountRole } from '@/flavours/glitch/models/account';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import IconPinned from '@/images/icons/icon_pinned.svg?react'; import IconPinned from '@/images/icons/icon_pinned.svg?react';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
@ -46,9 +44,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
return null; return null;
} }
const isRedesign = isRedesignEnabled();
const className = isRedesign ? classes.badge : '';
const domain = account.acct.includes('@') const domain = account.acct.includes('@')
? account.acct.split('@')[1] ? account.acct.split('@')[1]
: localDomain; : localDomain;
@ -58,7 +53,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
<AdminBadge <AdminBadge
key={role.id} key={role.id}
label={role.name} label={role.name}
className={className} className={classes.badge}
domain={`(${domain})`} domain={`(${domain})`}
roleId={role.id} roleId={role.id}
/>, />,
@ -68,8 +63,8 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
<Badge <Badge
key={role.id} key={role.id}
label={role.name} label={role.name}
className={className} className={classes.badge}
domain={isRedesign ? `(${domain})` : domain} domain={`(${domain})`}
roleId={role.id} roleId={role.id}
/>, />,
); );
@ -77,17 +72,17 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
}); });
if (account.bot) { if (account.bot) {
badges.push(<AutomatedBadge key='bot-badge' className={className} />); badges.push(<AutomatedBadge key='bot-badge' className={classes.badge} />);
} }
if (account.group) { if (account.group) {
badges.push(<GroupBadge key='group-badge' className={className} />); badges.push(<GroupBadge key='group-badge' className={classes.badge} />);
} }
if (isRedesign && relationship) { if (relationship) {
if (relationship.blocking) { if (relationship.blocking) {
badges.push( badges.push(
<BlockedBadge <BlockedBadge
key='blocking' key='blocking'
className={classNames(className, classes.badgeBlocked)} className={classNames(classes.badge, classes.badgeBlocked)}
/>, />,
); );
} }
@ -95,7 +90,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
badges.push( badges.push(
<BlockedBadge <BlockedBadge
key='domain-blocking' key='domain-blocking'
className={classNames(className, classes.badgeBlocked)} className={classNames(classes.badge, classes.badgeBlocked)}
domain={domain} domain={domain}
label={ label={
<FormattedMessage <FormattedMessage
@ -110,7 +105,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
badges.push( badges.push(
<MutedBadge <MutedBadge
key='muted-badge' key='muted-badge'
className={classNames(className, classes.badgeMuted)} className={classNames(classes.badge, classes.badgeMuted)}
expiresAt={relationship.muting_expires_at} expiresAt={relationship.muting_expires_at}
/>, />,
); );
@ -136,5 +131,5 @@ export const PinnedBadge: FC = () => (
function isAdminBadge(role: AccountRole) { function isAdminBadge(role: AccountRole) {
const name = role.name.toLowerCase(); const name = role.name.toLowerCase();
return isRedesignEnabled() && (name === 'admin' || name === 'owner'); return name === 'admin' || name === 'owner';
} }

View File

@ -16,8 +16,6 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react'; import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react'; import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { isRedesignEnabled } from '../common';
import { AccountMenu } from './menu'; import { AccountMenu } from './menu';
const messages = defineMessages({ const messages = defineMessages({
@ -97,7 +95,6 @@ const AccountButtonsOther: FC<
accountId={accountId} accountId={accountId}
className='account__header__follow-button' className='account__header__follow-button'
labelLength='long' labelLength='long'
withUnmute={!isRedesignEnabled()}
/> />
)} )}
{isFollowing && ( {isFollowing && (

View File

@ -1,68 +1,30 @@
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import type { FC } from 'react'; import type { FC } from 'react';
import { defineMessage, FormattedMessage, useIntl } from 'react-intl'; import { defineMessage, useIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { openModal } from '@/flavours/glitch/actions/modal'; import { openModal } from '@/flavours/glitch/actions/modal';
import { AccountFields } from '@/flavours/glitch/components/account_fields';
import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context'; import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context';
import type { EmojiHTMLProps } from '@/flavours/glitch/components/emoji/html'; import type { EmojiHTMLProps } from '@/flavours/glitch/components/emoji/html';
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html'; import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_date';
import { Icon } from '@/flavours/glitch/components/icon'; import { Icon } from '@/flavours/glitch/components/icon';
import { IconButton } from '@/flavours/glitch/components/icon_button'; import { IconButton } from '@/flavours/glitch/components/icon_button';
import { MiniCard } from '@/flavours/glitch/components/mini_card'; import { MiniCard } from '@/flavours/glitch/components/mini_card';
import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link'; import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useResizeObserver } from '@/flavours/glitch/hooks/useObserver'; import { useResizeObserver } from '@/flavours/glitch/hooks/useObserver';
import type { Account } from '@/flavours/glitch/models/account';
import { useAppDispatch } from '@/flavours/glitch/store'; import { useAppDispatch } from '@/flavours/glitch/store';
import IconVerified from '@/images/icons/icon_verified.svg?react'; import IconVerified from '@/images/icons/icon_verified.svg?react';
import MoreIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { cleanExtraEmojis } from '../../emoji/normalize'; import { cleanExtraEmojis } from '../../emoji/normalize';
import type { AccountField } from '../common'; import type { AccountField } from '../common';
import { isRedesignEnabled } from '../common';
import { useFieldHtml } from '../hooks/useFieldHtml'; import { useFieldHtml } from '../hooks/useFieldHtml';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
export const AccountHeaderFields: FC<{ accountId: string }> = ({
accountId,
}) => {
const account = useAccount(accountId);
if (!account) {
return null;
}
if (isRedesignEnabled()) {
return <RedesignAccountHeaderFields account={account} />;
}
return (
<div className='account__header__fields'>
<dl>
<dt>
<FormattedMessage id='account.joined_short' defaultMessage='Joined' />
</dt>
<dd>
<FormattedDateWrapper
value={account.created_at}
year='numeric'
month='short'
day='2-digit'
/>
</dd>
</dl>
<AccountFields fields={account.fields} emojis={account.emojis} />
</div>
);
};
const verifyMessage = defineMessage({ const verifyMessage = defineMessage({
id: 'account.link_verified_on', id: 'account.link_verified_on',
defaultMessage: 'Ownership of this link was checked on {date}', defaultMessage: 'Ownership of this link was checked on {date}',
@ -75,13 +37,22 @@ const dateFormatOptions: Intl.DateTimeFormatOptions = {
minute: '2-digit', minute: '2-digit',
}; };
const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => { export const AccountHeaderFields: FC<{ accountId: string }> = ({
accountId,
}) => {
const account = useAccount(accountId);
const emojis = useMemo( const emojis = useMemo(
() => cleanExtraEmojis(account.emojis), () => cleanExtraEmojis(account?.emojis),
[account.emojis], [account?.emojis],
); );
const accountFields = account?.fields;
const fields: AccountField[] = useMemo(() => { const fields: AccountField[] = useMemo(() => {
const fields = account.fields.toJS(); const fields = accountFields?.toJS();
if (!fields) {
return [];
}
if (!emojis) { if (!emojis) {
return fields.map((field) => ({ return fields.map((field) => ({
...field, ...field,
@ -102,10 +73,10 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
field.value_plain?.includes(`:${code}:`), field.value_plain?.includes(`:${code}:`),
), ),
})); }));
}, [account.fields, emojis]); }, [accountFields, emojis]);
const htmlHandlers = useElementHandledLink({ const htmlHandlers = useElementHandledLink({
hashtagAccountId: account.id, hashtagAccountId: account?.id,
}); });
const { wrapperRef } = useColumnWrap(); const { wrapperRef } = useColumnWrap();

View File

@ -4,13 +4,9 @@ import { FormattedMessage } from 'react-intl';
import type { Relationship } from '@/flavours/glitch/models/relationship'; import type { Relationship } from '@/flavours/glitch/models/relationship';
import { isRedesignEnabled } from '../common';
export const AccountInfo: FC<{ relationship?: Relationship }> = ({ export const AccountInfo: FC<{ relationship?: Relationship }> = ({
relationship, relationship,
}) => { }) => {
const isRedesign = isRedesignEnabled();
if (!relationship) { if (!relationship) {
return null; return null;
} }
@ -21,17 +17,17 @@ export const AccountInfo: FC<{ relationship?: Relationship }> = ({
<AccountInfoFollower relationship={relationship} /> <AccountInfoFollower relationship={relationship} />
</span> </span>
)} )}
{!isRedesign && relationship.blocking && ( {relationship.blocking && (
<span className='relationship-tag'> <span className='relationship-tag'>
<FormattedMessage id='account.blocking' defaultMessage='Blocking' /> <FormattedMessage id='account.blocking' defaultMessage='Blocking' />
</span> </span>
)} )}
{!isRedesign && relationship.muting && ( {relationship.muting && (
<span key='muting' className='relationship-tag'> <span key='muting' className='relationship-tag'>
<FormattedMessage id='account.muting' defaultMessage='Muting' /> <FormattedMessage id='account.muting' defaultMessage='Muting' />
</span> </span>
)} )}
{!isRedesign && relationship.domain_blocking && ( {relationship.domain_blocking && (
<span key='domain_blocking' className='relationship-tag'> <span key='domain_blocking' className='relationship-tag'>
<FormattedMessage <FormattedMessage
id='account.domain_blocking' id='account.domain_blocking'

View File

@ -43,8 +43,6 @@ import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react'
import ReportIcon from '@/material-icons/400-24px/report.svg?react'; import ReportIcon from '@/material-icons/400-24px/report.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react'; import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
@ -66,19 +64,9 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
return []; return [];
} }
if (isRedesignEnabled()) { return redesignMenuItems({
return redesignMenuItems({
account,
signedIn: !isMe && signedIn,
permissions,
intl,
relationship,
dispatch,
});
}
return currentMenuItems({
account, account,
signedIn, signedIn: !isMe && signedIn,
permissions, permissions,
intl, intl,
relationship, relationship,
@ -181,269 +169,6 @@ const messages = defineMessages({
}, },
}); });
function currentMenuItems({
account,
signedIn,
permissions,
intl,
relationship,
dispatch,
}: MenuItemsParams): MenuItem[] {
const items: MenuItem[] = [];
const isRemote = account.acct !== account.username;
if (signedIn && !account.suspended) {
items.push(
{
text: intl.formatMessage(messages.mention, {
name: account.username,
}),
action: () => {
dispatch(mentionCompose(account));
},
},
{
text: intl.formatMessage(messages.direct, {
name: account.username,
}),
action: () => {
dispatch(directCompose(account));
},
},
null,
);
}
if (isRemote) {
items.push(
{
text: intl.formatMessage(messages.openOriginalPage),
href: account.url,
},
null,
);
}
if (!signedIn) {
return items;
}
if (relationship?.following) {
// Timeline options
if (!relationship.muting) {
if (relationship.showing_reblogs) {
items.push({
text: intl.formatMessage(messages.hideReblogs, {
name: account.username,
}),
action: () => {
dispatch(followAccount(account.id, { reblogs: false }));
},
});
} else {
items.push({
text: intl.formatMessage(messages.showReblogs, {
name: account.username,
}),
action: () => {
dispatch(followAccount(account.id, { reblogs: true }));
},
});
}
items.push(
{
text: intl.formatMessage(messages.languages),
action: () => {
dispatch(
openModal({
modalType: 'SUBSCRIBED_LANGUAGES',
modalProps: {
accountId: account.id,
},
}),
);
},
},
null,
);
}
items.push(
{
text: intl.formatMessage(
relationship.endorsed ? messages.unendorse : messages.endorse,
),
action: () => {
if (relationship.endorsed) {
dispatch(unpinAccount(account.id));
} else {
dispatch(pinAccount(account.id));
}
},
},
{
text: intl.formatMessage(messages.add_or_remove_from_list),
action: () => {
dispatch(
openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: account.id,
},
}),
);
},
},
null,
);
}
if (relationship?.followed_by) {
const handleRemoveFromFollowers = () => {
dispatch(
openModal({
modalType: 'CONFIRM',
modalProps: {
title: intl.formatMessage(messages.confirmRemoveFromFollowersTitle),
message: intl.formatMessage(
messages.confirmRemoveFromFollowersMessage,
{ name: <strong>{account.acct}</strong> },
),
confirm: intl.formatMessage(
messages.confirmRemoveFromFollowersButton,
),
onConfirm: () => {
void dispatch(
removeAccountFromFollowers({ accountId: account.id }),
);
},
},
}),
);
};
items.push({
text: intl.formatMessage(messages.removeFromFollowers, {
name: account.username,
}),
action: handleRemoveFromFollowers,
dangerous: true,
});
}
if (relationship?.muting) {
items.push({
text: intl.formatMessage(messages.unmute, {
name: account.username,
}),
action: () => {
dispatch(unmuteAccount(account.id));
},
});
} else {
items.push({
text: intl.formatMessage(messages.mute, {
name: account.username,
}),
action: () => {
dispatch(initMuteModal(account));
},
dangerous: true,
});
}
if (relationship?.blocking) {
items.push({
text: intl.formatMessage(messages.unblock, {
name: account.username,
}),
action: () => {
dispatch(unblockAccount(account.id));
},
});
} else {
items.push({
text: intl.formatMessage(messages.block, {
name: account.username,
}),
action: () => {
dispatch(blockAccount(account.id));
},
dangerous: true,
});
}
if (!account.suspended) {
items.push({
text: intl.formatMessage(messages.report, {
name: account.username,
}),
action: () => {
dispatch(initReport(account));
},
dangerous: true,
});
}
const remoteDomain = isRemote ? account.acct.split('@')[1] : null;
if (remoteDomain) {
items.push(null);
if (relationship?.domain_blocking) {
items.push({
text: intl.formatMessage(messages.unblockDomain, {
domain: remoteDomain,
}),
action: () => {
dispatch(unblockDomain(remoteDomain));
},
});
} else {
items.push({
text: intl.formatMessage(messages.blockDomain, {
domain: remoteDomain,
}),
action: () => {
dispatch(initDomainBlockModal(account));
},
dangerous: true,
});
}
}
if (
(permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS ||
(isRemote &&
(permissions & PERMISSION_MANAGE_FEDERATION) ===
PERMISSION_MANAGE_FEDERATION)
) {
items.push(null);
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
items.push({
text: intl.formatMessage(messages.admin_account, {
name: account.username,
}),
href: `/admin/accounts/${account.id}`,
});
}
if (
isRemote &&
(permissions & PERMISSION_MANAGE_FEDERATION) ===
PERMISSION_MANAGE_FEDERATION
) {
items.push({
text: intl.formatMessage(messages.admin_domain, {
domain: remoteDomain,
}),
href: `/admin/instances/${remoteDomain}`,
});
}
}
return items;
}
const redesignMessages = defineMessages({ const redesignMessages = defineMessages({
share: { id: 'account.menu.share', defaultMessage: 'Share…' }, share: { id: 'account.menu.share', defaultMessage: 'Share…' },
copy: { id: 'account.menu.copy', defaultMessage: 'Copy link' }, copy: { id: 'account.menu.copy', defaultMessage: 'Copy link' },

View File

@ -3,13 +3,6 @@ import type { FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { NavLink } from 'react-router-dom';
import {
FollowersCounter,
FollowingCounter,
StatusesCounter,
} from '@/flavours/glitch/components/counters';
import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_date'; import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_date';
import { import {
NumberFields, NumberFields,
@ -18,54 +11,9 @@ import {
import { ShortNumber } from '@/flavours/glitch/components/short_number'; import { ShortNumber } from '@/flavours/glitch/components/short_number';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { isRedesignEnabled } from '../common'; export const AccountNumberFields: FC<{ accountId: string }> = ({
accountId,
const LegacyNumberFields: FC<{ accountId: string }> = ({ accountId }) => { }) => {
const intl = useIntl();
const account = useAccount(accountId);
if (!account) {
return null;
}
return (
<div className='account__header__extra__links'>
<NavLink
to={`/@${account.acct}`}
title={intl.formatNumber(account.statuses_count)}
>
<ShortNumber
value={account.statuses_count}
renderer={StatusesCounter}
/>
</NavLink>
<NavLink
exact
to={`/@${account.acct}/following`}
title={intl.formatNumber(account.following_count)}
>
<ShortNumber
value={account.following_count}
renderer={FollowingCounter}
/>
</NavLink>
<NavLink
exact
to={`/@${account.acct}/followers`}
title={intl.formatNumber(account.followers_count)}
>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
/>
</NavLink>
</div>
);
};
const RedesignNumberFields: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl(); const intl = useIntl();
const account = useAccount(accountId); const account = useAccount(accountId);
const createdThisYear = useMemo( const createdThisYear = useMemo(
@ -125,7 +73,3 @@ const RedesignNumberFields: FC<{ accountId: string }> = ({ accountId }) => {
</NumberFields> </NumberFields>
); );
}; };
export const AccountNumberFields = isRedesignEnabled()
? RedesignNumberFields
: LegacyNumberFields;

View File

@ -391,3 +391,65 @@ svg.badgeIcon {
padding-bottom: 14px; padding-bottom: 14px;
} }
} }
.bannerBase {
box-sizing: border-box;
padding: 16px;
border-radius: 12px;
background: var(--color-bg-secondary);
display: flex;
flex-direction: column;
gap: 12px;
justify-content: center;
align-items: flex-start;
margin: 16px 0;
}
.bannerTextAndActions {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
font-weight: 400;
color: var(--color-text-primary);
h2 {
font-size: 17px;
font-weight: 600;
}
}
.bannerDisclaimer {
color: var(--color-text-secondary);
font-size: 11px;
a {
color: inherit;
}
}
.bannerBaseCentered {
composes: bannerBase;
min-height: 146px;
align-items: center;
.bannerTextAndActions {
text-align: center;
}
}
.bannerInputButton {
display: flex;
gap: 8px;
align-self: stretch;
align-items: flex-start;
& > div {
flex-grow: 1;
}
input[type='email'] {
padding: 7px 8px; // To align size with button
background: var(--color-bg-primary);
}
}

View File

@ -8,40 +8,13 @@ import { NavLink } from 'react-router-dom';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useAccountId } from '@/flavours/glitch/hooks/useAccountId'; import { useAccountId } from '@/flavours/glitch/hooks/useAccountId';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
export const AccountTabs: FC<{ acct: string }> = ({ acct }) => {
if (isRedesignEnabled()) {
return <RedesignTabs />;
}
return (
<div className='account__section-headline'>
<NavLink exact to={`/@${acct}/featured`}>
<FormattedMessage id='account.featured' defaultMessage='Featured' />
</NavLink>
<NavLink exact to={`/@${acct}`}>
<FormattedMessage id='account.posts' defaultMessage='Posts' />
</NavLink>
<NavLink exact to={`/@${acct}/with_replies`}>
<FormattedMessage
id='account.posts_with_replies'
defaultMessage='Posts and replies'
/>
</NavLink>
<NavLink exact to={`/@${acct}/media`}>
<FormattedMessage id='account.media' defaultMessage='Media' />
</NavLink>
</div>
);
};
const isActive: Required<NavLinkProps>['isActive'] = (match, location) => const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
match?.url === location.pathname || match?.url === location.pathname ||
(!!match?.url && location.pathname.startsWith(`${match.url}/tagged/`)); (!!match?.url && location.pathname.startsWith(`${match.url}/tagged/`));
const RedesignTabs: FC = () => { export const AccountTabs: FC = () => {
const accountId = useAccountId(); const accountId = useAccountId();
const account = useAccount(accountId); const account = useAccount(accountId);

View File

@ -28,6 +28,7 @@ export const AccountFieldModal: FC<{
as='h2' as='h2'
htmlString={field.name_emojified} htmlString={field.name_emojified}
onElement={handleLabelElement} onElement={handleLabelElement}
className={classes.fieldName}
/> />
<EmojiHTML <EmojiHTML
as='p' as='p'

View File

@ -20,6 +20,11 @@
outline-offset: 2px; outline-offset: 2px;
} }
.fieldName,
.fieldValue {
word-break: break-all;
}
.fieldValue { .fieldValue {
color: var(--color-text-primary); color: var(--color-text-primary);
font-weight: 600; font-weight: 600;

View File

@ -23,7 +23,7 @@ export const AccountFilters: FC = () => {
} }
return ( return (
<> <>
<AccountTabs acct={acct} /> <AccountTabs />
<div className={classes.filtersWrapper}> <div className={classes.filtersWrapper}>
<FilterDropdown /> <FilterDropdown />
</div> </div>

View File

@ -18,7 +18,6 @@ import { selectTimelineByKey } from '@/flavours/glitch/selectors/timelines';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import IconPinned from '@/images/icons/icon_pinned.svg?react'; import IconPinned from '@/images/icons/icon_pinned.svg?react';
import { isRedesignEnabled } from '../common';
import { PinnedBadge } from '../components/badges'; import { PinnedBadge } from '../components/badges';
import { useAccountContext } from './context'; import { useAccountContext } from './context';
@ -88,10 +87,6 @@ export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
export const PinnedShowAllButton: FC = () => { export const PinnedShowAllButton: FC = () => {
const { onShowAllPinned } = useAccountContext(); const { onShowAllPinned } = useAccountContext();
if (!isRedesignEnabled()) {
return null;
}
return ( return (
<Button <Button
onClick={onShowAllPinned} onClick={onShowAllPinned}

View File

@ -36,7 +36,10 @@ export const AccountStatusHeader: FC<StatusHeaderProps> = ({
className='status__relative-time' className='status__relative-time'
> >
<StatusVisibility visibility={status.get('visibility')} /> <StatusVisibility visibility={status.get('visibility')} />
<RelativeTimestamp timestamp={status.get('created_at') as string} /> <RelativeTimestamp
timestamp={status.get('created_at') as string}
noFuture
/>
{editedAt && <StatusEditedAt editedAt={editedAt} />} {editedAt && <StatusEditedAt editedAt={editedAt} />}
</Link> </Link>

View File

@ -72,7 +72,7 @@ const messages = defineMessages({
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' }, statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
tootHeading: { id: 'account.posts_with_replies', defaultMessage: 'Posts and replies' }, tootHeading: { id: 'account.filters.posts_replies', defaultMessage: 'Posts and replies' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {

View File

@ -27,7 +27,6 @@ import { layoutFromWindow } from 'flavours/glitch/is_mobile';
import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications'; import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import { checkAnnualReport } from '@/flavours/glitch/reducers/slices/annual_report'; import { checkAnnualReport } from '@/flavours/glitch/reducers/slices/annual_report';
import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache'; import { clearHeight } from '../../actions/height_cache';
@ -190,20 +189,6 @@ class SwitchingColumnsArea extends PureComponent {
rootRedirect = '/about'; rootRedirect = '/about';
} }
const profileRedesignRoutes = [];
if (isServerFeatureEnabled('profile_redesign')) {
profileRedesignRoutes.push(
<WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />,
<WrappedRoute key="featured_tags" path='/profile/featured_tags' component={AccountEditFeaturedTags} content={children} />
)
} else {
// If profile editing is not enabled, redirect to the home timeline as the current editing pages are outside React Router.
profileRedesignRoutes.push(
<Redirect key="edit-redirect" from='/profile/edit' to='/' exact />,
<Redirect key="featured-tags-redirect" from='/profile/featured_tags' to='/' exact />,
);
}
return ( return (
<ColumnsContextProvider multiColumn={!singleColumn}> <ColumnsContextProvider multiColumn={!singleColumn}>
<ColumnsArea ref={this.setRef} singleColumn={singleColumn}> <ColumnsArea ref={this.setRef} singleColumn={singleColumn}>
@ -250,7 +235,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/search' component={Search} content={children} /> <WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} /> <WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
{...profileRedesignRoutes} <WrappedRoute path='/profile/edit' component={AccountEdit} content={children} />
<WrappedRoute path='/profile/featured_tags' component={AccountEditFeaturedTags} content={children} />
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} /> <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} /> <WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />

View File

@ -1,5 +1,3 @@
import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
export function EmojiPicker () { export function EmojiPicker () {
return import('../../emoji/emoji_picker'); return import('../../emoji/emoji_picker');
} }
@ -79,10 +77,7 @@ export function PinnedStatuses () {
} }
export function AccountTimeline () { export function AccountTimeline () {
if (isServerFeatureEnabled('profile_redesign')) { return import('../../account_timeline/v2');
return import('../../account_timeline/v2');
}
return import('../../account_timeline');
} }
export function AccountGallery () { export function AccountGallery () {

View File

@ -101,6 +101,7 @@ export const accountDefaultValues: AccountShape = {
limited: false, limited: false,
moved: null, moved: null,
hide_collections: false, hide_collections: false,
email_subscriptions: false,
// This comes from `ApiMutedAccountJSON`, but we should eventually // This comes from `ApiMutedAccountJSON`, but we should eventually
// store that in a different object. // store that in a different object.
mute_expires_at: null, mute_expires_at: null,

View File

@ -8725,7 +8725,6 @@ noscript {
&__bar { &__bar {
position: relative; position: relative;
padding: 0 20px; padding: 0 20px;
padding-bottom: 16px; // glitch-soc addition for the different tabs design
border-bottom: 1px solid var(--color-border-primary); border-bottom: 1px solid var(--color-border-primary);
.avatar { .avatar {

View File

@ -12,7 +12,7 @@ export function isProduction() {
else return import.meta.env.PROD; else return import.meta.env.PROD;
} }
export type ServerFeatures = 'fasp' | 'collections' | 'profile_redesign'; export type ServerFeatures = 'fasp' | 'collections';
export function isServerFeatureEnabled(feature: ServerFeatures) { export function isServerFeatureEnabled(feature: ServerFeatures) {
return initialState?.features.includes(feature) ?? false; return initialState?.features.includes(feature) ?? false;

View File

@ -75,3 +75,6 @@ export const apiDeleteProfileAvatar = () =>
export const apiDeleteProfileHeader = () => export const apiDeleteProfileHeader = () =>
apiRequestDelete('v1/profile/header'); apiRequestDelete('v1/profile/header');
export const apiSubscribeByEmail = (id: string, email: string) =>
apiRequestPost(`v1/accounts/${id}/email_subscriptions`, { email });

View File

@ -68,6 +68,7 @@ export interface BaseApiAccountJSON {
limited?: boolean; limited?: boolean;
memorial?: boolean; memorial?: boolean;
hide_collections: boolean; hide_collections: boolean;
email_subscriptions?: boolean;
} }
// See app/serializers/rest/muted_account_serializer.rb // See app/serializers/rest/muted_account_serializer.rb

View File

@ -0,0 +1,22 @@
export type ErrorToken =
| 'ERR_TAKEN'
| 'ERR_INVALID'
| 'ERR_BLOCKED'
| 'ERR_RESERVED'
| 'ERR_TOO_MANY'
| 'ERR_MALFORMED'
| 'ERR_UNUSABLE'
| 'ERR_TOO_SOON'
| 'ERR_BELOW_LIMIT'
| 'ERR_UNREACHABLE'
| 'ERR_ELEVATED';
export interface ValidationError {
error: ErrorToken;
description: string;
}
export interface ValidationErrorResponse {
error: string;
details: Record<string, ValidationError[]>;
}

View File

@ -6,7 +6,6 @@ import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useIdentity } from '@/mastodon/identity_context'; import { useIdentity } from '@/mastodon/identity_context';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { import {
fetchRelationships, fetchRelationships,
followAccount, followAccount,
@ -171,23 +170,10 @@ export const FollowButton: React.FC<{
'button--compact': compact, 'button--compact': compact,
}); });
if (isServerFeatureEnabled('profile_redesign')) {
return (
<Link to='/profile/edit' className={buttonClasses}>
{label}
</Link>
);
}
return ( return (
<a <Link to='/profile/edit' className={buttonClasses}>
href='/settings/profile'
target='_blank'
rel='noopener'
className={buttonClasses}
>
{label} {label}
</a> </Link>
); );
} }

View File

@ -23,6 +23,7 @@
a { a {
padding: 0; padding: 0;
text-decoration: none;
&:hover, &:hover,
&:focus { &:focus {

View File

@ -56,7 +56,10 @@ export const StatusHeader: FC<StatusHeaderProps> = ({
className='status__relative-time' className='status__relative-time'
> >
<StatusVisibility visibility={status.get('visibility')} /> <StatusVisibility visibility={status.get('visibility')} />
<RelativeTimestamp timestamp={status.get('created_at') as string} /> <RelativeTimestamp
timestamp={status.get('created_at') as string}
noFuture
/>
{editedAt && <StatusEditedAt editedAt={editedAt} />} {editedAt && <StatusEditedAt editedAt={editedAt} />}
</Link> </Link>

View File

@ -210,7 +210,7 @@ export const EditFieldModal = forwardRef<
const labelStatus = checkField(newLabel); const labelStatus = checkField(newLabel);
const valueStatus = checkField(newValue); const valueStatus = checkField(newValue);
if (labelStatus || valueStatus) { if (labelStatus?.variant === 'error' || valueStatus?.variant === 'error') {
setFieldStatuses({ setFieldStatuses({
label: labelStatus ?? undefined, label: labelStatus ?? undefined,
value: valueStatus ?? undefined, value: valueStatus ?? undefined,

View File

@ -182,6 +182,11 @@
// Field component // Field component
.fieldName,
.fieldValue {
word-break: break-all;
}
.fieldName { .fieldName {
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 13px; font-size: 13px;

View File

@ -7,7 +7,6 @@ import { useHistory } from 'react-router';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccount } from '@/mastodon/hooks/useAccount';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts'; import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags'; import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
import { Account } from 'mastodon/components/account'; import { Account } from 'mastodon/components/account';
@ -49,11 +48,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
const history = useHistory(); const history = useHistory();
useEffect(() => { useEffect(() => {
if ( if (account && !account.show_featured) {
account &&
!account.show_featured &&
isServerFeatureEnabled('profile_redesign')
) {
history.push(`/@${account.acct}`); history.push(`/@${account.acct}`);
} }
}, [account, history]); }, [account, history]);
@ -111,8 +106,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
); );
} }
const noTags = const noTags = featuredTags.isEmpty();
featuredTags.isEmpty() || isServerFeatureEnabled('profile_redesign');
if ( if (
noTags && noTags &&

View File

@ -4,7 +4,6 @@ import { FormattedMessage } from 'react-intl';
import { List as ImmutableList, isList } from 'immutable'; import { List as ImmutableList, isList } from 'immutable';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines'; import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button'; import { ColumnBackButton } from 'mastodon/components/column_back_button';
@ -27,8 +26,6 @@ import { MediaItem } from './components/media_item';
const emptyList = ImmutableList<MediaAttachment>(); const emptyList = ImmutableList<MediaAttachment>();
const redesignEnabled = isServerFeatureEnabled('profile_redesign');
const selectGalleryTimeline = createAppSelector( const selectGalleryTimeline = createAppSelector(
[ [
(_state, accountId?: string | null) => accountId, (_state, accountId?: string | null) => accountId,
@ -58,7 +55,7 @@ const selectGalleryTimeline = createAppSelector(
const { show_media, show_media_replies } = account; const { show_media, show_media_replies } = account;
// If the account disabled showing media, don't display anything. // If the account disabled showing media, don't display anything.
if (!show_media && redesignEnabled) { if (!show_media) {
return { return {
items, items,
hasMore: false, hasMore: false,
@ -67,7 +64,7 @@ const selectGalleryTimeline = createAppSelector(
}; };
} }
const withReplies = show_media_replies && redesignEnabled; const withReplies = show_media_replies;
const timeline = timelines.get( const timeline = timelines.get(
`account:${accountId}:media${withReplies ? ':with_replies' : ''}`, `account:${accountId}:media${withReplies ? ':with_replies' : ''}`,
); );
@ -225,7 +222,7 @@ export const AccountGallery: React.FC<{
alwaysPrepend alwaysPrepend
append={accountId && <RemoteHint accountId={accountId} />} append={accountId && <RemoteHint accountId={accountId} />}
scrollKey='account_gallery' scrollKey='account_gallery'
showLoading={isLoading} isLoading={isLoading}
hasMore={!forceEmptyState && hasMore} hasMore={!forceEmptyState && hasMore}
onLoadMore={handleLoadMore} onLoadMore={handleLoadMore}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

View File

@ -1,9 +1,4 @@
import type { AccountFieldShape } from '@/mastodon/models/account'; import type { AccountFieldShape } from '@/mastodon/models/account';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
export function isRedesignEnabled() {
return isServerFeatureEnabled('profile_redesign');
}
export interface AccountField extends AccountFieldShape { export interface AccountField extends AccountFieldShape {
nameHasEmojis: boolean; nameHasEmojis: boolean;

View File

@ -7,7 +7,6 @@ import { openModal } from '@/mastodon/actions/modal';
import { AccountBio } from '@/mastodon/components/account_bio'; import { AccountBio } from '@/mastodon/components/account_bio';
import { Avatar } from '@/mastodon/components/avatar'; import { Avatar } from '@/mastodon/components/avatar';
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
import { AccountNote } from '@/mastodon/features/account/components/account_note';
import FollowRequestNoteContainer from '@/mastodon/features/account/containers/follow_request_note_container'; import FollowRequestNoteContainer from '@/mastodon/features/account/containers/follow_request_note_container';
import { useLayout } from '@/mastodon/hooks/useLayout'; import { useLayout } from '@/mastodon/hooks/useLayout';
import { useVisibility } from '@/mastodon/hooks/useVisibility'; import { useVisibility } from '@/mastodon/hooks/useVisibility';
@ -20,14 +19,12 @@ import type { Account } from '@/mastodon/models/account';
import { getAccountHidden } from '@/mastodon/selectors/accounts'; import { getAccountHidden } from '@/mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from '@/mastodon/store'; import { useAppSelector, useAppDispatch } from '@/mastodon/store';
import { isRedesignEnabled } from '../common';
import { AccountName } from './account_name'; import { AccountName } from './account_name';
import { AccountSubscriptionForm } from './account_subscription_form';
import { AccountBadges } from './badges'; import { AccountBadges } from './badges';
import { AccountButtons } from './buttons'; import { AccountButtons } from './buttons';
import { FamiliarFollowers } from './familiar_followers'; import { FamiliarFollowers } from './familiar_followers';
import { AccountHeaderFields } from './fields'; import { AccountHeaderFields } from './fields';
import { AccountInfo } from './info';
import { MemorialNote } from './memorial_note'; import { MemorialNote } from './memorial_note';
import { MovedNote } from './moved_note'; import { MovedNote } from './moved_note';
import { AccountNote as AccountNoteRedesign } from './note'; import { AccountNote as AccountNoteRedesign } from './note';
@ -51,8 +48,6 @@ export const AccountHeader: React.FC<{
accountId: string; accountId: string;
hideTabs?: boolean; hideTabs?: boolean;
}> = ({ accountId, hideTabs }) => { }> = ({ accountId, hideTabs }) => {
const isRedesign = isRedesignEnabled();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const account = useAppSelector((state) => state.accounts.get(accountId)); const account = useAppSelector((state) => state.accounts.get(accountId));
const relationship = useAppSelector((state) => const relationship = useAppSelector((state) =>
@ -119,13 +114,9 @@ export const AccountHeader: React.FC<{
<div <div
className={classNames( className={classNames(
'account__header__image', 'account__header__image',
isRedesign && redesignClasses.header, redesignClasses.header,
)} )}
> >
{me !== account.id && relationship && !isRedesign && (
<AccountInfo relationship={relationship} />
)}
{!suspendedOrHidden && ( {!suspendedOrHidden && (
<img <img
src={autoPlayGif ? account.header : account.header_static} src={autoPlayGif ? account.header : account.header_static}
@ -138,13 +129,13 @@ export const AccountHeader: React.FC<{
<div <div
className={classNames( className={classNames(
'account__header__bar', 'account__header__bar',
isRedesign && redesignClasses.barWrapper, redesignClasses.barWrapper,
)} )}
> >
<div <div
className={classNames( className={classNames(
'account__header__tabs', 'account__header__tabs',
isRedesign && redesignClasses.avatarWrapper, redesignClasses.avatarWrapper,
)} )}
> >
<a <a
@ -156,33 +147,24 @@ export const AccountHeader: React.FC<{
> >
<Avatar <Avatar
account={suspendedOrHidden ? undefined : account} account={suspendedOrHidden ? undefined : account}
size={isRedesign ? 80 : 92} size={80}
/> />
</a> </a>
{!isRedesign && (
<AccountButtons
accountId={accountId}
className='account__header__buttons--desktop'
/>
)}
</div> </div>
<div <div
className={classNames( className={classNames(
'account__header__tabs__name', 'account__header__tabs__name',
isRedesign && redesignClasses.nameWrapper, redesignClasses.nameWrapper,
)} )}
> >
<AccountName accountId={accountId} /> <AccountName accountId={accountId} />
{isRedesign && ( <AccountButtons
<AccountButtons accountId={accountId}
accountId={accountId} className={redesignClasses.buttonsDesktop}
className={redesignClasses.buttonsDesktop} noShare={!isMe || 'share' in navigator}
noShare={!isMe || 'share' in navigator} forceMenu={'share' in navigator}
forceMenu={'share' in navigator} />
/>
)}
</div> </div>
<AccountBadges accountId={accountId} /> <AccountBadges accountId={accountId} />
@ -191,54 +173,45 @@ export const AccountHeader: React.FC<{
<FamiliarFollowers accountId={accountId} /> <FamiliarFollowers accountId={accountId} />
)} )}
{!isRedesign && (
<AccountButtons
className='account__header__buttons--mobile'
accountId={accountId}
noShare
/>
)}
{!suspendedOrHidden && ( {!suspendedOrHidden && (
<div className='account__header__extra'> <div className='account__header__extra'>
<div className='account__header__bio'> <div className='account__header__bio'>
{me && {me && account.id !== me && (
account.id !== me && <AccountNoteRedesign accountId={accountId} />
(isRedesign ? ( )}
<AccountNoteRedesign accountId={accountId} />
) : (
<AccountNote accountId={accountId} />
))}
<AccountBio <AccountBio
showDropdown showDropdown
accountId={accountId} accountId={accountId}
className={classNames( className={classNames(
'account__header__content', 'account__header__content',
isRedesign && redesignClasses.bio, redesignClasses.bio,
)} )}
/> />
<AccountHeaderFields accountId={accountId} /> <AccountHeaderFields accountId={accountId} />
</div> </div>
{!me && account.email_subscriptions && (
<AccountSubscriptionForm accountId={accountId} />
)}
<AccountNumberFields accountId={accountId} /> <AccountNumberFields accountId={accountId} />
</div> </div>
)} )}
{isRedesign && ( <AccountButtons
<AccountButtons className={classNames(
className={classNames( redesignClasses.buttonsMobile,
redesignClasses.buttonsMobile, !isIntersecting && redesignClasses.buttonsMobileIsStuck,
!isIntersecting && redesignClasses.buttonsMobileIsStuck, )}
)} accountId={accountId}
accountId={accountId} noShare
noShare />
/>
)}
</div> </div>
</AnimateEmojiProvider> </AnimateEmojiProvider>
{!hideTabs && !hidden && <AccountTabs acct={account.acct} />} {!hideTabs && !hidden && <AccountTabs />}
<div ref={observedRef} /> <div ref={observedRef} />
<Helmet> <Helmet>

View File

@ -14,10 +14,6 @@ import { useAppSelector } from '@/mastodon/store';
import AtIcon from '@/material-icons/400-24px/alternate_email.svg?react'; import AtIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import HelpIcon from '@/material-icons/400-24px/help.svg?react'; import HelpIcon from '@/material-icons/400-24px/help.svg?react';
import DomainIcon from '@/material-icons/400-24px/language.svg?react'; import DomainIcon from '@/material-icons/400-24px/language.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import { DomainPill } from '../../account/components/domain_pill';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
@ -34,7 +30,6 @@ const messages = defineMessages({
}); });
export const AccountName: FC<{ accountId: string }> = ({ accountId }) => { export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl();
const account = useAccount(accountId); const account = useAccount(accountId);
const me = useAppSelector((state) => state.meta.get('me') as string); const me = useAppSelector((state) => state.meta.get('me') as string);
const localDomain = useAppSelector( const localDomain = useAppSelector(
@ -47,32 +42,6 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
const [username = '', domain = localDomain] = account.acct.split('@'); const [username = '', domain = localDomain] = account.acct.split('@');
if (!isRedesignEnabled()) {
return (
<h1>
<DisplayName account={account} variant='simple' />
<small>
<span>
@{username}
<span className='invisible'>@{domain}</span>
</span>
<DomainPill
username={username}
domain={domain}
isSelf={me === account.id}
/>
{account.locked && (
<Icon
id='lock'
icon={LockIcon}
aria-label={intl.formatMessage(messages.lockedInfo)}
/>
)}
</small>
</h1>
);
}
return ( return (
<div className={classes.name}> <div className={classes.name}>
<h1> <h1>

View File

@ -0,0 +1,207 @@
import { useState, useCallback, useId } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import type { IntlShape } from 'react-intl';
import { Link } from 'react-router-dom';
import { AxiosError } from 'axios';
import { apiSubscribeByEmail } from 'mastodon/api/accounts';
import type {
ValidationErrorResponse,
ValidationError,
} from 'mastodon/api_types/errors';
import { A11yLiveRegion } from 'mastodon/components/a11y_live_region';
import { Button } from 'mastodon/components/button';
import { CalloutInline } from 'mastodon/components/callout_inline';
import { DisplayName } from 'mastodon/components/display_name';
import type { FieldStatus } from 'mastodon/components/form_fields';
import formFieldClasses from 'mastodon/components/form_fields/form_field_wrapper.module.scss';
import { TextInput } from 'mastodon/components/form_fields/text_input_field';
import { useAppSelector } from 'mastodon/store';
import classes from './redesign.module.scss';
const messages = defineMessages({
emailInvalid: {
id: 'email_subscriptions.validation.email.invalid',
defaultMessage: 'Invalid email address',
},
emailBlocked: {
id: 'email_subscriptions.validation.email.blocked',
defaultMessage: 'Blocked email provider',
},
email: {
id: 'email_subscriptions.email',
defaultMessage: 'Email address',
},
});
const isValidationErrorResponse = (
data: unknown,
): data is ValidationErrorResponse =>
typeof data === 'object' &&
data !== null &&
'error' in data &&
'details' in data;
const fieldStatusFromErrors = (
intl: IntlShape,
errors: ValidationError[],
): FieldStatus | undefined => {
const error = errors[0];
if (!error) {
return undefined;
}
let message: string;
switch (error.error) {
case 'ERR_BLOCKED':
message = intl.formatMessage(messages.emailBlocked);
break;
case 'ERR_INVALID':
default:
message = intl.formatMessage(messages.emailInvalid);
break;
}
return { variant: 'error', message };
};
export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
accountId,
}) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
const intl = useIntl();
const accessibilityId = useId();
const [email, setEmail] = useState('');
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [errors, setErrors] = useState<Record<string, ValidationError[]>>({});
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(e) => {
setEmail(e.target.value);
setErrors({});
},
[],
);
const handleSubmit = useCallback<React.FormEventHandler>(
(e) => {
e.preventDefault();
if (email.length === 0) {
return;
}
setSubmitting(true);
apiSubscribeByEmail(accountId, email)
.then(() => {
setSubmitting(false);
setSubmitted(true);
return '';
})
.catch((err: unknown) => {
setSubmitting(false);
if (err instanceof AxiosError && err.response) {
const data: unknown = err.response.data;
if (isValidationErrorResponse(data)) {
if (data.details.email?.some((k) => k.error === 'ERR_TAKEN')) {
setSubmitted(true);
return;
}
setErrors(data.details);
}
}
});
},
[accountId, email],
);
if (submitted) {
return (
<div className={classes.bannerBaseCentered}>
<div className={classes.bannerTextAndActions}>
<h2>
<FormattedMessage
id='email_subscriptions.submitted.title'
defaultMessage='One more step'
/>
</h2>
<FormattedMessage
id='email_subscriptions.submitted.lead'
defaultMessage='Check your inbox for an email to finish signing up for email updates.'
/>
</div>
</div>
);
}
return (
<form onSubmit={handleSubmit} className={classes.bannerBase} noValidate>
<div className={classes.bannerTextAndActions}>
<h2>
<FormattedMessage
id='email_subscriptions.form.title'
defaultMessage='Sign up for email updates from {name}'
values={{
name: <DisplayName account={account} variant='simple' />,
}}
/>
</h2>
<FormattedMessage
id='email_subscriptions.form.lead'
defaultMessage='Get posts in your inbox without creating a Mastodon account.'
/>
</div>
<div className={classes.bannerInputButton}>
<div className={formFieldClasses.wrapper}>
<TextInput
id={`${accessibilityId}-input`}
type='email'
value={email}
onChange={handleChange}
placeholder='name@email.com'
aria-label={intl.formatMessage(messages.email)}
aria-describedby={errors.email ? `${accessibilityId}-status` : ''}
/>
<A11yLiveRegion
className={formFieldClasses.status}
id={`${accessibilityId}-status`}
>
{errors.email && (
<CalloutInline {...fieldStatusFromErrors(intl, errors.email)} />
)}
</A11yLiveRegion>
</div>
<Button type='submit' loading={submitting}>
<FormattedMessage
id='email_subscriptions.form.action'
defaultMessage='Subscribe'
/>
</Button>
</div>
<div className={classes.bannerDisclaimer}>
<FormattedMessage
id='email_subscriptions.form.disclaimer'
defaultMessage='You can unsubscribe at any time. For more information, refer to the <a>Privacy Policy</a>.'
values={{ a: (str) => <Link to='/privacy-policy'>{str}</Link> }}
/>
</div>
</form>
);
};

View File

@ -20,8 +20,6 @@ import { useAccount } from '@/mastodon/hooks/useAccount';
import type { AccountRole } from '@/mastodon/models/account'; import type { AccountRole } from '@/mastodon/models/account';
import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
@ -46,9 +44,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
return null; return null;
} }
const isRedesign = isRedesignEnabled();
const className = isRedesign ? classes.badge : '';
const domain = account.acct.includes('@') const domain = account.acct.includes('@')
? account.acct.split('@')[1] ? account.acct.split('@')[1]
: localDomain; : localDomain;
@ -58,7 +53,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
<AdminBadge <AdminBadge
key={role.id} key={role.id}
label={role.name} label={role.name}
className={className} className={classes.badge}
domain={`(${domain})`} domain={`(${domain})`}
roleId={role.id} roleId={role.id}
/>, />,
@ -68,8 +63,8 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
<Badge <Badge
key={role.id} key={role.id}
label={role.name} label={role.name}
className={className} className={classes.badge}
domain={isRedesign ? `(${domain})` : domain} domain={`(${domain})`}
roleId={role.id} roleId={role.id}
/>, />,
); );
@ -77,17 +72,17 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
}); });
if (account.bot) { if (account.bot) {
badges.push(<AutomatedBadge key='bot-badge' className={className} />); badges.push(<AutomatedBadge key='bot-badge' className={classes.badge} />);
} }
if (account.group) { if (account.group) {
badges.push(<GroupBadge key='group-badge' className={className} />); badges.push(<GroupBadge key='group-badge' className={classes.badge} />);
} }
if (isRedesign && relationship) { if (relationship) {
if (relationship.blocking) { if (relationship.blocking) {
badges.push( badges.push(
<BlockedBadge <BlockedBadge
key='blocking' key='blocking'
className={classNames(className, classes.badgeBlocked)} className={classNames(classes.badge, classes.badgeBlocked)}
/>, />,
); );
} }
@ -95,7 +90,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
badges.push( badges.push(
<BlockedBadge <BlockedBadge
key='domain-blocking' key='domain-blocking'
className={classNames(className, classes.badgeBlocked)} className={classNames(classes.badge, classes.badgeBlocked)}
domain={domain} domain={domain}
label={ label={
<FormattedMessage <FormattedMessage
@ -110,7 +105,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
badges.push( badges.push(
<MutedBadge <MutedBadge
key='muted-badge' key='muted-badge'
className={classNames(className, classes.badgeMuted)} className={classNames(classes.badge, classes.badgeMuted)}
expiresAt={relationship.muting_expires_at} expiresAt={relationship.muting_expires_at}
/>, />,
); );
@ -136,5 +131,5 @@ export const PinnedBadge: FC = () => (
function isAdminBadge(role: AccountRole) { function isAdminBadge(role: AccountRole) {
const name = role.name.toLowerCase(); const name = role.name.toLowerCase();
return isRedesignEnabled() && (name === 'admin' || name === 'owner'); return name === 'admin' || name === 'owner';
} }

View File

@ -16,8 +16,6 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react'; import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react'; import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { isRedesignEnabled } from '../common';
import { AccountMenu } from './menu'; import { AccountMenu } from './menu';
const messages = defineMessages({ const messages = defineMessages({
@ -97,7 +95,6 @@ const AccountButtonsOther: FC<
accountId={accountId} accountId={accountId}
className='account__header__follow-button' className='account__header__follow-button'
labelLength='long' labelLength='long'
withUnmute={!isRedesignEnabled()}
/> />
)} )}
{isFollowing && ( {isFollowing && (

View File

@ -1,68 +1,30 @@
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import type { FC } from 'react'; import type { FC } from 'react';
import { defineMessage, FormattedMessage, useIntl } from 'react-intl'; import { defineMessage, useIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import IconVerified from '@/images/icons/icon_verified.svg?react'; import IconVerified from '@/images/icons/icon_verified.svg?react';
import { openModal } from '@/mastodon/actions/modal'; import { openModal } from '@/mastodon/actions/modal';
import { AccountFields } from '@/mastodon/components/account_fields';
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context'; import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
import type { EmojiHTMLProps } from '@/mastodon/components/emoji/html'; import type { EmojiHTMLProps } from '@/mastodon/components/emoji/html';
import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { EmojiHTML } from '@/mastodon/components/emoji/html';
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
import { Icon } from '@/mastodon/components/icon'; import { Icon } from '@/mastodon/components/icon';
import { IconButton } from '@/mastodon/components/icon_button'; import { IconButton } from '@/mastodon/components/icon_button';
import { MiniCard } from '@/mastodon/components/mini_card'; import { MiniCard } from '@/mastodon/components/mini_card';
import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccount } from '@/mastodon/hooks/useAccount';
import { useResizeObserver } from '@/mastodon/hooks/useObserver'; import { useResizeObserver } from '@/mastodon/hooks/useObserver';
import type { Account } from '@/mastodon/models/account';
import { useAppDispatch } from '@/mastodon/store'; import { useAppDispatch } from '@/mastodon/store';
import MoreIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { cleanExtraEmojis } from '../../emoji/normalize'; import { cleanExtraEmojis } from '../../emoji/normalize';
import type { AccountField } from '../common'; import type { AccountField } from '../common';
import { isRedesignEnabled } from '../common';
import { useFieldHtml } from '../hooks/useFieldHtml'; import { useFieldHtml } from '../hooks/useFieldHtml';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
export const AccountHeaderFields: FC<{ accountId: string }> = ({
accountId,
}) => {
const account = useAccount(accountId);
if (!account) {
return null;
}
if (isRedesignEnabled()) {
return <RedesignAccountHeaderFields account={account} />;
}
return (
<div className='account__header__fields'>
<dl>
<dt>
<FormattedMessage id='account.joined_short' defaultMessage='Joined' />
</dt>
<dd>
<FormattedDateWrapper
value={account.created_at}
year='numeric'
month='short'
day='2-digit'
/>
</dd>
</dl>
<AccountFields fields={account.fields} emojis={account.emojis} />
</div>
);
};
const verifyMessage = defineMessage({ const verifyMessage = defineMessage({
id: 'account.link_verified_on', id: 'account.link_verified_on',
defaultMessage: 'Ownership of this link was checked on {date}', defaultMessage: 'Ownership of this link was checked on {date}',
@ -75,13 +37,22 @@ const dateFormatOptions: Intl.DateTimeFormatOptions = {
minute: '2-digit', minute: '2-digit',
}; };
const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => { export const AccountHeaderFields: FC<{ accountId: string }> = ({
accountId,
}) => {
const account = useAccount(accountId);
const emojis = useMemo( const emojis = useMemo(
() => cleanExtraEmojis(account.emojis), () => cleanExtraEmojis(account?.emojis),
[account.emojis], [account?.emojis],
); );
const accountFields = account?.fields;
const fields: AccountField[] = useMemo(() => { const fields: AccountField[] = useMemo(() => {
const fields = account.fields.toJS(); const fields = accountFields?.toJS();
if (!fields) {
return [];
}
if (!emojis) { if (!emojis) {
return fields.map((field) => ({ return fields.map((field) => ({
...field, ...field,
@ -102,10 +73,10 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
field.value_plain?.includes(`:${code}:`), field.value_plain?.includes(`:${code}:`),
), ),
})); }));
}, [account.fields, emojis]); }, [accountFields, emojis]);
const htmlHandlers = useElementHandledLink({ const htmlHandlers = useElementHandledLink({
hashtagAccountId: account.id, hashtagAccountId: account?.id,
}); });
const { wrapperRef } = useColumnWrap(); const { wrapperRef } = useColumnWrap();

View File

@ -40,8 +40,6 @@ import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react'
import ReportIcon from '@/material-icons/400-24px/report.svg?react'; import ReportIcon from '@/material-icons/400-24px/report.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react'; import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
@ -63,19 +61,9 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
return []; return [];
} }
if (isRedesignEnabled()) { return redesignMenuItems({
return redesignMenuItems({
account,
signedIn: !isMe && signedIn,
permissions,
intl,
relationship,
dispatch,
});
}
return currentMenuItems({
account, account,
signedIn, signedIn: !isMe && signedIn,
permissions, permissions,
intl, intl,
relationship, relationship,
@ -178,269 +166,6 @@ const messages = defineMessages({
}, },
}); });
function currentMenuItems({
account,
signedIn,
permissions,
intl,
relationship,
dispatch,
}: MenuItemsParams): MenuItem[] {
const items: MenuItem[] = [];
const isRemote = account.acct !== account.username;
if (signedIn && !account.suspended) {
items.push(
{
text: intl.formatMessage(messages.mention, {
name: account.username,
}),
action: () => {
dispatch(mentionCompose(account));
},
},
{
text: intl.formatMessage(messages.direct, {
name: account.username,
}),
action: () => {
dispatch(directCompose(account));
},
},
null,
);
}
if (isRemote) {
items.push(
{
text: intl.formatMessage(messages.openOriginalPage),
href: account.url,
},
null,
);
}
if (!signedIn) {
return items;
}
if (relationship?.following) {
// Timeline options
if (!relationship.muting) {
if (relationship.showing_reblogs) {
items.push({
text: intl.formatMessage(messages.hideReblogs, {
name: account.username,
}),
action: () => {
dispatch(followAccount(account.id, { reblogs: false }));
},
});
} else {
items.push({
text: intl.formatMessage(messages.showReblogs, {
name: account.username,
}),
action: () => {
dispatch(followAccount(account.id, { reblogs: true }));
},
});
}
items.push(
{
text: intl.formatMessage(messages.languages),
action: () => {
dispatch(
openModal({
modalType: 'SUBSCRIBED_LANGUAGES',
modalProps: {
accountId: account.id,
},
}),
);
},
},
null,
);
}
items.push(
{
text: intl.formatMessage(
relationship.endorsed ? messages.unendorse : messages.endorse,
),
action: () => {
if (relationship.endorsed) {
dispatch(unpinAccount(account.id));
} else {
dispatch(pinAccount(account.id));
}
},
},
{
text: intl.formatMessage(messages.add_or_remove_from_list),
action: () => {
dispatch(
openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: account.id,
},
}),
);
},
},
null,
);
}
if (relationship?.followed_by) {
const handleRemoveFromFollowers = () => {
dispatch(
openModal({
modalType: 'CONFIRM',
modalProps: {
title: intl.formatMessage(messages.confirmRemoveFromFollowersTitle),
message: intl.formatMessage(
messages.confirmRemoveFromFollowersMessage,
{ name: <strong>{account.acct}</strong> },
),
confirm: intl.formatMessage(
messages.confirmRemoveFromFollowersButton,
),
onConfirm: () => {
void dispatch(
removeAccountFromFollowers({ accountId: account.id }),
);
},
},
}),
);
};
items.push({
text: intl.formatMessage(messages.removeFromFollowers, {
name: account.username,
}),
action: handleRemoveFromFollowers,
dangerous: true,
});
}
if (relationship?.muting) {
items.push({
text: intl.formatMessage(messages.unmute, {
name: account.username,
}),
action: () => {
dispatch(unmuteAccount(account.id));
},
});
} else {
items.push({
text: intl.formatMessage(messages.mute, {
name: account.username,
}),
action: () => {
dispatch(initMuteModal(account));
},
dangerous: true,
});
}
if (relationship?.blocking) {
items.push({
text: intl.formatMessage(messages.unblock, {
name: account.username,
}),
action: () => {
dispatch(unblockAccount(account.id));
},
});
} else {
items.push({
text: intl.formatMessage(messages.block, {
name: account.username,
}),
action: () => {
dispatch(blockAccount(account.id));
},
dangerous: true,
});
}
if (!account.suspended) {
items.push({
text: intl.formatMessage(messages.report, {
name: account.username,
}),
action: () => {
dispatch(initReport(account));
},
dangerous: true,
});
}
const remoteDomain = isRemote ? account.acct.split('@')[1] : null;
if (remoteDomain) {
items.push(null);
if (relationship?.domain_blocking) {
items.push({
text: intl.formatMessage(messages.unblockDomain, {
domain: remoteDomain,
}),
action: () => {
dispatch(unblockDomain(remoteDomain));
},
});
} else {
items.push({
text: intl.formatMessage(messages.blockDomain, {
domain: remoteDomain,
}),
action: () => {
dispatch(initDomainBlockModal(account));
},
dangerous: true,
});
}
}
if (
(permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS ||
(isRemote &&
(permissions & PERMISSION_MANAGE_FEDERATION) ===
PERMISSION_MANAGE_FEDERATION)
) {
items.push(null);
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
items.push({
text: intl.formatMessage(messages.admin_account, {
name: account.username,
}),
href: `/admin/accounts/${account.id}`,
});
}
if (
isRemote &&
(permissions & PERMISSION_MANAGE_FEDERATION) ===
PERMISSION_MANAGE_FEDERATION
) {
items.push({
text: intl.formatMessage(messages.admin_domain, {
domain: remoteDomain,
}),
href: `/admin/instances/${remoteDomain}`,
});
}
}
return items;
}
const redesignMessages = defineMessages({ const redesignMessages = defineMessages({
share: { id: 'account.menu.share', defaultMessage: 'Share…' }, share: { id: 'account.menu.share', defaultMessage: 'Share…' },
copy: { id: 'account.menu.copy', defaultMessage: 'Copy link' }, copy: { id: 'account.menu.copy', defaultMessage: 'Copy link' },

View File

@ -3,13 +3,6 @@ import type { FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { NavLink } from 'react-router-dom';
import {
FollowersCounter,
FollowingCounter,
StatusesCounter,
} from '@/mastodon/components/counters';
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
import { import {
NumberFields, NumberFields,
@ -18,54 +11,9 @@ import {
import { ShortNumber } from '@/mastodon/components/short_number'; import { ShortNumber } from '@/mastodon/components/short_number';
import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccount } from '@/mastodon/hooks/useAccount';
import { isRedesignEnabled } from '../common'; export const AccountNumberFields: FC<{ accountId: string }> = ({
accountId,
const LegacyNumberFields: FC<{ accountId: string }> = ({ accountId }) => { }) => {
const intl = useIntl();
const account = useAccount(accountId);
if (!account) {
return null;
}
return (
<div className='account__header__extra__links'>
<NavLink
to={`/@${account.acct}`}
title={intl.formatNumber(account.statuses_count)}
>
<ShortNumber
value={account.statuses_count}
renderer={StatusesCounter}
/>
</NavLink>
<NavLink
exact
to={`/@${account.acct}/following`}
title={intl.formatNumber(account.following_count)}
>
<ShortNumber
value={account.following_count}
renderer={FollowingCounter}
/>
</NavLink>
<NavLink
exact
to={`/@${account.acct}/followers`}
title={intl.formatNumber(account.followers_count)}
>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
/>
</NavLink>
</div>
);
};
const RedesignNumberFields: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl(); const intl = useIntl();
const account = useAccount(accountId); const account = useAccount(accountId);
const createdThisYear = useMemo( const createdThisYear = useMemo(
@ -125,7 +73,3 @@ const RedesignNumberFields: FC<{ accountId: string }> = ({ accountId }) => {
</NumberFields> </NumberFields>
); );
}; };
export const AccountNumberFields = isRedesignEnabled()
? RedesignNumberFields
: LegacyNumberFields;

View File

@ -391,3 +391,65 @@ svg.badgeIcon {
padding-bottom: 14px; padding-bottom: 14px;
} }
} }
.bannerBase {
box-sizing: border-box;
padding: 16px;
border-radius: 12px;
background: var(--color-bg-secondary);
display: flex;
flex-direction: column;
gap: 12px;
justify-content: center;
align-items: flex-start;
margin: 16px 0;
}
.bannerTextAndActions {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
font-weight: 400;
color: var(--color-text-primary);
h2 {
font-size: 17px;
font-weight: 600;
}
}
.bannerDisclaimer {
color: var(--color-text-secondary);
font-size: 11px;
a {
color: inherit;
}
}
.bannerBaseCentered {
composes: bannerBase;
min-height: 146px;
align-items: center;
.bannerTextAndActions {
text-align: center;
}
}
.bannerInputButton {
display: flex;
gap: 8px;
align-self: stretch;
align-items: flex-start;
& > div {
flex-grow: 1;
}
input[type='email'] {
padding: 7px 8px; // To align size with button
background: var(--color-bg-primary);
}
}

View File

@ -8,40 +8,13 @@ import { NavLink } from 'react-router-dom';
import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountId } from '@/mastodon/hooks/useAccountId'; import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
export const AccountTabs: FC<{ acct: string }> = ({ acct }) => {
if (isRedesignEnabled()) {
return <RedesignTabs />;
}
return (
<div className='account__section-headline'>
<NavLink exact to={`/@${acct}/featured`}>
<FormattedMessage id='account.featured' defaultMessage='Featured' />
</NavLink>
<NavLink exact to={`/@${acct}`}>
<FormattedMessage id='account.posts' defaultMessage='Posts' />
</NavLink>
<NavLink exact to={`/@${acct}/with_replies`}>
<FormattedMessage
id='account.posts_with_replies'
defaultMessage='Posts and replies'
/>
</NavLink>
<NavLink exact to={`/@${acct}/media`}>
<FormattedMessage id='account.media' defaultMessage='Media' />
</NavLink>
</div>
);
};
const isActive: Required<NavLinkProps>['isActive'] = (match, location) => const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
match?.url === location.pathname || match?.url === location.pathname ||
(!!match?.url && location.pathname.startsWith(`${match.url}/tagged/`)); (!!match?.url && location.pathname.startsWith(`${match.url}/tagged/`));
const RedesignTabs: FC = () => { export const AccountTabs: FC = () => {
const accountId = useAccountId(); const accountId = useAccountId();
const account = useAccount(accountId); const account = useAccount(accountId);

View File

@ -28,6 +28,7 @@ export const AccountFieldModal: FC<{
as='h2' as='h2'
htmlString={field.name_emojified} htmlString={field.name_emojified}
onElement={handleLabelElement} onElement={handleLabelElement}
className={classes.fieldName}
/> />
<EmojiHTML <EmojiHTML
as='p' as='p'

View File

@ -20,6 +20,11 @@
outline-offset: 2px; outline-offset: 2px;
} }
.fieldName,
.fieldValue {
word-break: break-all;
}
.fieldValue { .fieldValue {
color: var(--color-text-primary); color: var(--color-text-primary);
font-weight: 600; font-weight: 600;

View File

@ -23,7 +23,7 @@ export const AccountFilters: FC = () => {
} }
return ( return (
<> <>
<AccountTabs acct={acct} /> <AccountTabs />
<div className={classes.filtersWrapper}> <div className={classes.filtersWrapper}>
<FilterDropdown /> <FilterDropdown />
</div> </div>

View File

@ -18,7 +18,6 @@ import type { StatusHeaderRenderFn } from '@/mastodon/components/status/header';
import { selectTimelineByKey } from '@/mastodon/selectors/timelines'; import { selectTimelineByKey } from '@/mastodon/selectors/timelines';
import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { isRedesignEnabled } from '../common';
import { PinnedBadge } from '../components/badges'; import { PinnedBadge } from '../components/badges';
import { useAccountContext } from './context'; import { useAccountContext } from './context';
@ -88,10 +87,6 @@ export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
export const PinnedShowAllButton: FC = () => { export const PinnedShowAllButton: FC = () => {
const { onShowAllPinned } = useAccountContext(); const { onShowAllPinned } = useAccountContext();
if (!isRedesignEnabled()) {
return null;
}
return ( return (
<Button <Button
onClick={onShowAllPinned} onClick={onShowAllPinned}

View File

@ -36,7 +36,10 @@ export const AccountStatusHeader: FC<StatusHeaderProps> = ({
className='status__relative-time' className='status__relative-time'
> >
<StatusVisibility visibility={status.get('visibility')} /> <StatusVisibility visibility={status.get('visibility')} />
<RelativeTimestamp timestamp={status.get('created_at') as string} /> <RelativeTimestamp
timestamp={status.get('created_at') as string}
noFuture
/>
{editedAt && <StatusEditedAt editedAt={editedAt} />} {editedAt && <StatusEditedAt editedAt={editedAt} />}
</Link> </Link>

View File

@ -24,7 +24,6 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
import { layoutFromWindow } from 'mastodon/is_mobile'; import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report'; import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache'; import { clearHeight } from '../../actions/height_cache';
@ -182,20 +181,6 @@ class SwitchingColumnsArea extends PureComponent {
rootRedirect = '/about'; rootRedirect = '/about';
} }
const profileRedesignRoutes = [];
if (isServerFeatureEnabled('profile_redesign')) {
profileRedesignRoutes.push(
<WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />,
<WrappedRoute key="featured_tags" path='/profile/featured_tags' component={AccountEditFeaturedTags} content={children} />
)
} else {
// If profile editing is not enabled, redirect to the home timeline as the current editing pages are outside React Router.
profileRedesignRoutes.push(
<Redirect key="edit-redirect" from='/profile/edit' to='/' exact />,
<Redirect key="featured-tags-redirect" from='/profile/featured_tags' to='/' exact />,
);
}
return ( return (
<ColumnsContextProvider multiColumn={!singleColumn}> <ColumnsContextProvider multiColumn={!singleColumn}>
<ColumnsArea ref={this.setRef} singleColumn={singleColumn}> <ColumnsArea ref={this.setRef} singleColumn={singleColumn}>
@ -242,7 +227,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/search' component={Search} content={children} /> <WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} /> <WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
{...profileRedesignRoutes} <WrappedRoute path='/profile/edit' component={AccountEdit} content={children} />
<WrappedRoute path='/profile/featured_tags' component={AccountEditFeaturedTags} content={children} />
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} /> <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} /> <WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />

View File

@ -1,5 +1,3 @@
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
export function EmojiPicker () { export function EmojiPicker () {
return import('../../emoji/emoji_picker'); return import('../../emoji/emoji_picker');
} }
@ -79,10 +77,7 @@ export function PinnedStatuses () {
} }
export function AccountTimeline () { export function AccountTimeline () {
if (isServerFeatureEnabled('profile_redesign')) { return import('../../account_timeline/v2');
return import('../../account_timeline/v2');
}
return import('../../account_timeline');
} }
export function AccountGallery () { export function AccountGallery () {

View File

@ -480,7 +480,7 @@
"confirmation_modal.cancel": "Άκυρο", "confirmation_modal.cancel": "Άκυρο",
"confirmations.block.confirm": "Αποκλεισμός", "confirmations.block.confirm": "Αποκλεισμός",
"confirmations.delete.confirm": "Διαγραφή", "confirmations.delete.confirm": "Διαγραφή",
"confirmations.delete.message": "Σίγουρα θες να διαγράψεις αυτή την ανάρτηση;", "confirmations.delete.message": "Σίγουρα θες να διαγράψεις αυτήν την ανάρτηση;",
"confirmations.delete.title": "Διαγραφή ανάρτησης;", "confirmations.delete.title": "Διαγραφή ανάρτησης;",
"confirmations.delete_collection.confirm": "Διαγραφή", "confirmations.delete_collection.confirm": "Διαγραφή",
"confirmations.delete_collection.message": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", "confirmations.delete_collection.message": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
@ -1138,7 +1138,7 @@
"search.search_or_paste": "Αναζήτηση ή εισαγωγή URL", "search.search_or_paste": "Αναζήτηση ή εισαγωγή URL",
"search_popout.full_text_search_disabled_message": "Μη διαθέσιμο στο {domain}.", "search_popout.full_text_search_disabled_message": "Μη διαθέσιμο στο {domain}.",
"search_popout.full_text_search_logged_out_message": "Διαθέσιμο μόνο όταν συνδεθείς.", "search_popout.full_text_search_logged_out_message": "Διαθέσιμο μόνο όταν συνδεθείς.",
"search_popout.language_code": "Κωδικός γλώσσας ISO", "search_popout.language_code": "κωδικός ISO γλώσσας",
"search_popout.options": "Επιλογές αναζήτησης", "search_popout.options": "Επιλογές αναζήτησης",
"search_popout.quick_actions": "Γρήγορες ενέργειες", "search_popout.quick_actions": "Γρήγορες ενέργειες",
"search_popout.recent": "Πρόσφατες αναζητήσεις", "search_popout.recent": "Πρόσφατες αναζητήσεις",

View File

@ -122,7 +122,6 @@
"account.note.title": "Personal note (visible only to you)", "account.note.title": "Personal note (visible only to you)",
"account.open_original_page": "Open original page", "account.open_original_page": "Open original page",
"account.posts": "Posts", "account.posts": "Posts",
"account.posts_with_replies": "Posts and replies",
"account.remove_from_followers": "Remove {name} from followers", "account.remove_from_followers": "Remove {name} from followers",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested_follow": "{name} has requested to follow you", "account.requested_follow": "{name} has requested to follow you",
@ -581,6 +580,15 @@
"domain_pill.your_server": "Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.", "domain_pill.your_server": "Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.",
"domain_pill.your_username": "Your unique identifier on this server. Its possible to find users with the same username on different servers.", "domain_pill.your_username": "Your unique identifier on this server. Its possible to find users with the same username on different servers.",
"dropdown.empty": "Select an option", "dropdown.empty": "Select an option",
"email_subscriptions.email": "Email address",
"email_subscriptions.form.action": "Subscribe",
"email_subscriptions.form.disclaimer": "You can unsubscribe at any time. For more information, refer to the <a>Privacy Policy</a>.",
"email_subscriptions.form.lead": "Get posts in your inbox without creating a Mastodon account.",
"email_subscriptions.form.title": "Sign up for email updates from {name}",
"email_subscriptions.submitted.lead": "Check your inbox for an email to finish signing up for email updates.",
"email_subscriptions.submitted.title": "One more step",
"email_subscriptions.validation.email.blocked": "Blocked email provider",
"email_subscriptions.validation.email.invalid": "Invalid email address",
"embed.instructions": "Embed this post on your website by copying the code below.", "embed.instructions": "Embed this post on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",

View File

@ -14,9 +14,16 @@
"about.powered_by": "Lìonra sòisealta sgaoilte le cumhachd {mastodon}", "about.powered_by": "Lìonra sòisealta sgaoilte le cumhachd {mastodon}",
"about.rules": "Riaghailtean an fhrithealaiche", "about.rules": "Riaghailtean an fhrithealaiche",
"account.account_note_header": "Nòta pearsanta", "account.account_note_header": "Nòta pearsanta",
"account.activity": "Gnìomhachd",
"account.add_note": "Cuir nòta pearsanta ris",
"account.add_or_remove_from_list": "Cuir ris no thoir air falbh o liostaichean", "account.add_or_remove_from_list": "Cuir ris no thoir air falbh o liostaichean",
"account.badges.admin": "Rianaire",
"account.badges.blocked": "Ga bhacadh",
"account.badges.bot": "Fèin-obrachail", "account.badges.bot": "Fèin-obrachail",
"account.badges.domain_blocked": "Àrainn bhacte",
"account.badges.group": "Buidheann", "account.badges.group": "Buidheann",
"account.badges.muted": "Ga mhùchadh",
"account.badges.muted_until": "Ga mhùchadh gu ruige {until}",
"account.block": "Bac @{name}", "account.block": "Bac @{name}",
"account.block_domain": "Bac an àrainn {domain}", "account.block_domain": "Bac an àrainn {domain}",
"account.block_short": "Bac", "account.block_short": "Bac",
@ -27,6 +34,7 @@
"account.direct": "Thoir iomradh air @{name} gu prìobhaideach", "account.direct": "Thoir iomradh air @{name} gu prìobhaideach",
"account.disable_notifications": "Na cuir brath thugam tuilleadh nuair a chuireas @{name} post ris", "account.disable_notifications": "Na cuir brath thugam tuilleadh nuair a chuireas @{name} post ris",
"account.domain_blocking": "Àrainn ga bacadh", "account.domain_blocking": "Àrainn ga bacadh",
"account.edit_note": "Deasaich an nòta pearsanta",
"account.edit_profile": "Deasaich a phròifil", "account.edit_profile": "Deasaich a phròifil",
"account.edit_profile_short": "Deasaich", "account.edit_profile_short": "Deasaich",
"account.enable_notifications": "Cuir brath thugam nuair a chuireas @{name} post ris", "account.enable_notifications": "Cuir brath thugam nuair a chuireas @{name} post ris",
@ -36,9 +44,17 @@
"account.familiar_followers_two": "Ga leantainn le {name1} s {name2}", "account.familiar_followers_two": "Ga leantainn le {name1} s {name2}",
"account.featured": "Ga bhrosnachadh", "account.featured": "Ga bhrosnachadh",
"account.featured.accounts": "Pròifilean", "account.featured.accounts": "Pròifilean",
"account.featured.collections": "Cruinneachaidhean",
"account.featured.hashtags": "Tagaichean hais", "account.featured.hashtags": "Tagaichean hais",
"account.featured_tags.last_status_at": "Am post mu dheireadh {date}", "account.featured_tags.last_status_at": "Am post mu dheireadh {date}",
"account.featured_tags.last_status_never": "Gun phost", "account.featured_tags.last_status_never": "Gun phost",
"account.field_overflow": "Seall an t-susbaint shlàn",
"account.filters.all": "A ghnìomhachd air fad",
"account.filters.boosts_toggle": "Seall na brosnachaidhean",
"account.filters.posts_boosts": "Postaichean s brosnachaidhean",
"account.filters.posts_only": "Postaichean",
"account.filters.posts_replies": "Postaichean s freagairtean",
"account.filters.replies_toggle": "Seall na freagairtean",
"account.follow": "Lean", "account.follow": "Lean",
"account.follow_back": "Lean air ais", "account.follow_back": "Lean air ais",
"account.follow_back_short": "Lean air ais", "account.follow_back_short": "Lean air ais",
@ -63,6 +79,24 @@
"account.locked_info": "Tha prìobhaideachd ghlaiste aig a chunntais seo. Nì an sealbhadair lèirmheas a làimh air cò dhfhaodas a leantainn.", "account.locked_info": "Tha prìobhaideachd ghlaiste aig a chunntais seo. Nì an sealbhadair lèirmheas a làimh air cò dhfhaodas a leantainn.",
"account.media": "Meadhanan", "account.media": "Meadhanan",
"account.mention": "Thoir iomradh air @{name}", "account.mention": "Thoir iomradh air @{name}",
"account.menu.add_to_list": "Cuir ri liosta…",
"account.menu.block": "Bac an cunntas",
"account.menu.block_domain": "Bac {domain}",
"account.menu.copied": "Chaidh lethbhreac de cheangal a chunntais a chur air an stòr-bhòrd",
"account.menu.copy": "Dèan lethbhreac dhen cheangal",
"account.menu.direct": "Thoir iomradh air gu prìobhaideach",
"account.menu.hide_reblogs": "Falaich na brosnachaidhean air an loidhne-ama",
"account.menu.mention": "Thoir iomradh",
"account.menu.mute": "Mùch an cunntas",
"account.menu.note.description": "Chan fhaic ach thu fhèin seo",
"account.menu.open_original_page": "Seall air {domain}",
"account.menu.remove_follower": "Thoir an neach-leantainn air falbh",
"account.menu.report": "Dèan gearan mun chunntas",
"account.menu.share": "Co-roinn…",
"account.menu.show_reblogs": "Seall na brosnachaidhean air an loidhne-ama",
"account.menu.unblock": "Dì-bhac an cunntas",
"account.menu.unblock_domain": "Dì-bhac {domain}",
"account.menu.unmute": "Dì-mhùch an cunntas",
"account.moved_to": "Dhinnis {name} gu bheil an cunntas ùr aca a-nis air:", "account.moved_to": "Dhinnis {name} gu bheil an cunntas ùr aca a-nis air:",
"account.mute": "Mùch @{name}", "account.mute": "Mùch @{name}",
"account.mute_notifications_short": "Mùch na brathan", "account.mute_notifications_short": "Mùch na brathan",
@ -70,7 +104,22 @@
"account.muted": "Ga mhùchadh", "account.muted": "Ga mhùchadh",
"account.muting": "Ga mhùchadh", "account.muting": "Ga mhùchadh",
"account.mutual": "A leantainn càch a chèile", "account.mutual": "A leantainn càch a chèile",
"account.name.help.domain": "Is {domain} am frithealaiche a tha ag òstadh pròifil s postaichean a chleachdaiche.",
"account.name.help.domain_self": "Is {domain} am frithealaiche agad-sa a tha ag òstadh pròifil s postaichean agad-sa.",
"account.name.help.footer": "Air an aon dòigh s a chuireas tu puist-d gu daoine le cliantan puist-d eadar-dhealaichte, s urrainn dhut conaltradh le daoine air frithealaichean Mastodon eile agus le duine sam bith air aplacaidean sòisealta eile a chleachdas na h-aon riaghailtean s a chleachdas Mastodon (sin pròtacal ActivityPub).",
"account.name.help.header": "Tha aithnichear coltach ri seòladh puist-d",
"account.name.help.username": "Is {username} ainm-cleachdaiche a chunntais seo air an fhrithealaiche aca-san. Dhfhaoidte gu bheil an t-aon ainm-cleachdaiche le cuideigin air frithealaiche eile.",
"account.name.help.username_self": "Is {username} d ainm-cleachdaiche air an fhrithealaiche seo. Dhfhaoidte gu bheil an t-aon ainm-cleachdaiche le cuideigin air frithealaiche eile.",
"account.name_info": "Dè s ciall dha seo?",
"account.no_bio": "Cha deach tuairisgeul a sholar.", "account.no_bio": "Cha deach tuairisgeul a sholar.",
"account.node_modal.callout": "Chan fhaic ach thu fhèin na nòtaichean pearsanta.",
"account.node_modal.edit_title": "Deasaich an nòta pearsanta",
"account.node_modal.error_unknown": "Cha b urrainn dhuinn an nòta a shàbhaladh",
"account.node_modal.field_label": "Nòta pearsanta",
"account.node_modal.save": "Sàbhail",
"account.node_modal.title": "Cuir nòta pearsanta ris",
"account.note.edit_button": "Deasaich",
"account.note.title": "Nòta pearsanta (chan fhaic ach thu fhèin e)",
"account.open_original_page": "Fosgail an duilleag thùsail", "account.open_original_page": "Fosgail an duilleag thùsail",
"account.posts": "Postaichean", "account.posts": "Postaichean",
"account.posts_with_replies": "Postaichean s freagairtean", "account.posts_with_replies": "Postaichean s freagairtean",
@ -81,6 +130,8 @@
"account.share": "Co-roinn a phròifil aig @{name}", "account.share": "Co-roinn a phròifil aig @{name}",
"account.show_reblogs": "Seall na brosnachaidhean o @{name}", "account.show_reblogs": "Seall na brosnachaidhean o @{name}",
"account.statuses_counter": "{count, plural, one {{counter} phost} two {{counter} phost} few {{counter} postaichean} other {{counter} post}}", "account.statuses_counter": "{count, plural, one {{counter} phost} two {{counter} phost} few {{counter} postaichean} other {{counter} post}}",
"account.timeline.pinned": "Prìnichte",
"account.timeline.pinned.view_all": "Seall na postaichean prìnichte uile",
"account.unblock": "Dì-bhac @{name}", "account.unblock": "Dì-bhac @{name}",
"account.unblock_domain": "Dì-bhac an àrainn {domain}", "account.unblock_domain": "Dì-bhac an àrainn {domain}",
"account.unblock_domain_short": "Dì-bhac", "account.unblock_domain_short": "Dì-bhac",
@ -90,6 +141,110 @@
"account.unmute": "Dì-mhùch @{name}", "account.unmute": "Dì-mhùch @{name}",
"account.unmute_notifications_short": "Dì-mhùch na brathan", "account.unmute_notifications_short": "Dì-mhùch na brathan",
"account.unmute_short": "Dì-mhùch", "account.unmute_short": "Dì-mhùch",
"account_edit.bio.add_label": "Cuir ris roinn mu mo dhèidhinn",
"account_edit.bio.edit_label": "Deasaich an roinn mu mo dhèidhinn",
"account_edit.bio.placeholder": "Cuir thu fhèin an aithne càich gu goirid.",
"account_edit.bio.title": "Mu mo dhèidhinn",
"account_edit.bio_modal.add_title": "Cuir ris roinn mu mo dhèidhinn",
"account_edit.bio_modal.edit_title": "Deasaich an roinn mu mo dhèidhinn",
"account_edit.column_button": "Deiseil",
"account_edit.column_title": "Deasaich a phròifil",
"account_edit.custom_fields.add_label": "Cuir raon ris",
"account_edit.custom_fields.edit_label": "Deasaich an raon",
"account_edit.custom_fields.placeholder": "Cuir ris do riochdairean, ceanglaichean dhan taobh a-muigh no rud sam bith eile a bu mhiann leat co-roinneadh.",
"account_edit.custom_fields.reorder_button": "Atharraich òrdugh nan raointean",
"account_edit.custom_fields.tip_content": "Cuir ri teistealachd do chunntais Mhastodon gun duilgheadas le dearbhadh cheanglaichean gu duilleag-lìn sam bith a tha leatsa.",
"account_edit.custom_fields.tip_title": "Gliocas: Cuir ceangalaichean dearbhte ris",
"account_edit.custom_fields.title": "Raointean gnàthaichte",
"account_edit.custom_fields.verified_hint": "Ciamar a chuireas mi ceangal dearbhte ris?",
"account_edit.display_name.add_label": "Cuir ris ainm-taisbeanaidh",
"account_edit.display_name.edit_label": "Deasaich an t-ainm-taisbeanaidh",
"account_edit.display_name.placeholder": "S e mar a nochdas d ainm air a phròifil agad agus air loidhnichean-ama a tha san ainm-taisbeanaidh agad.",
"account_edit.display_name.title": "Ainm-taisbeanaidh",
"account_edit.featured_hashtags.edit_label": "Cuir tagaichean hais ris",
"account_edit.featured_hashtags.placeholder": "Cuidich càch ach an aithnich iad na cuspairean as fheàrr leat s gum faigh iad grèim orra gu sgiobalta.",
"account_edit.featured_hashtags.title": "Tagaichean hais brosnaichte",
"account_edit.field_actions.delete": "Sguab às an raon",
"account_edit.field_actions.edit": "Deasaich an raon",
"account_edit.field_delete_modal.confirm": "A bheil thu cinnteach gu bheil thu airson an raon gnàthaichte seo a sguabadh às? Cha ghabh seo a neo-dhèanamh.",
"account_edit.field_delete_modal.delete_button": "Sguab às",
"account_edit.field_delete_modal.title": "A bheil thu airson an raon gnàthaichte a sguabadh às?",
"account_edit.field_edit_modal.add_title": "Cuir raon gnàthaichte ris",
"account_edit.field_edit_modal.discard_confirm": "Tilg air falbh",
"account_edit.field_edit_modal.discard_message": "Tha atharraichean gun sàbhaladh agad. A bheil thu cinnteach gu bheil airson an tilgeil air falbh?",
"account_edit.field_edit_modal.edit_title": "Deasaich an raoin gnàthaichte",
"account_edit.field_edit_modal.length_warning": "Chaidh thu thar crìoch nan caractaran a mholamaid. Dhfhaoidte nach fhaicear an raon agad gu lèir air mobile.",
"account_edit.field_edit_modal.link_emoji_warning": "Mholamaid nach cleachd thu emojis gnàthaichte le URLaichean. Thèid raointean gnàthaichte sa bheil an dà chuid còmhla a shealltainn nan teacsa a-mhàin seach nan ceangal ach nach cuireamaid an luchd-cleachdaidh tro chèile.",
"account_edit.field_edit_modal.name_hint": "Can “Làrach-lìn phearsanta”",
"account_edit.field_edit_modal.name_label": "Leubail",
"account_edit.field_edit_modal.url_warning": "Airson ceangal a chur ris, gabh a-staigh {protocol} aig a thoiseach.",
"account_edit.field_edit_modal.value_hint": "Can “https://example.me”",
"account_edit.field_edit_modal.value_label": "Luach",
"account_edit.field_reorder_modal.drag_cancel": "Chaidh sgur dhen t-slaodadh. Chaidh an raon “{item}” a leigeil às.",
"account_edit.field_reorder_modal.drag_end": "Chaidh an raon “{item}” a leigeil às.",
"account_edit.field_reorder_modal.drag_instructions": "Airson òrdugh nan raointean gnàthaichte atharrachadh, brùth air space no enter. Fhad s a bhios tu ri slaodadh, cleachd na h-iuchraichean saighde a ghluasad an raoin suas no sìos. Brùth air space no enter a-rithist a leigeil às an raoin air an ionad ùr aige no brùth air escape airson sgur dheth.",
"account_edit.field_reorder_modal.drag_move": "Chaidh an raon “{item}” a ghluasad.",
"account_edit.field_reorder_modal.drag_over": "Chaidh an raon “{item}” a ghluasad thar “{over}”.",
"account_edit.field_reorder_modal.drag_start": "Chaidh an raon “{item}” a thogail.",
"account_edit.field_reorder_modal.handle_label": "Slaod an raon “{item}”",
"account_edit.field_reorder_modal.title": "Cuir òrdugh ùr air na raointean",
"account_edit.image_alt_modal.add_title": "Cuir roghainn teacsa ris",
"account_edit.image_alt_modal.details_content": "MHOLAMAID: <ul> <li>Gun doir thu dealbh ort</li> <li>Gun cleachd thu cainnt treas pearsa (can “Ailig” seach “mise”)</li> <li>Nach cleachd thu ach beagan fhaclan</li> </ul> SEACHAINN: <ul> <li>Tòiseachadh le “Dealbh-camara de” bhiodh sin anabarrach do leughadairean-sgrìn</li> </ul> BALL-EISIMPLEIR: <ul> <li>“Ailig a caitheamh lèine ghorm agus speuclairean”</li> </ul>",
"account_edit.image_alt_modal.details_title": "Gliocasan: Roghainn teacsa air dealbhan pròifile",
"account_edit.image_alt_modal.edit_title": "Deasaich an roghainn teacsa",
"account_edit.image_alt_modal.text_hint": "Cuidichidh roghainn teacsa ach an tuigeadh ann fheadhainn a chleachdas leughadair-sgrìn do shusbaint.",
"account_edit.image_alt_modal.text_label": "Roghainn teacsa",
"account_edit.image_delete_modal.confirm": "A bheil thu cinnteach gu bheil thu airson an dealbh seo a sguabadh às? Cha ghabh seo a neo-dhèanamh.",
"account_edit.image_delete_modal.delete_button": "Sguab às",
"account_edit.image_delete_modal.title": "A bheil thu airson an dealbh a sguabadh às?",
"account_edit.image_edit.add_button": "Cuir dealbh ris",
"account_edit.image_edit.alt_add_button": "Cuir roghainn teacsa ris",
"account_edit.image_edit.alt_edit_button": "Deasaich an roghainn teacsa",
"account_edit.image_edit.remove_button": "Thoir air falbh an dealbh",
"account_edit.image_edit.replace_button": "Cuir dealbh ùr na àite",
"account_edit.item_list.delete": "Sguab às {name}",
"account_edit.item_list.edit": "Deasaich {name}",
"account_edit.name_modal.add_title": "Cuir ris ainm-taisbeanaidh",
"account_edit.name_modal.edit_title": "Deasaich an t-ainm-taisbeanaidh",
"account_edit.profile_tab.button_label": "Gnàthaich",
"account_edit.profile_tab.hint.description": "Gnàthaichidh na roghainnean seo na chì an luchd-cleachdaidh air {server} sna h-aplacaidean oifigeil ach dhfhaoidte nach bi iad an sàs dhan luchd-cleachdaidh air frithealaichean eile is aplacaidean treas-phàrtaidh.",
"account_edit.profile_tab.hint.title": "Bidh na chithear fhathast eadar-dhealaichte",
"account_edit.profile_tab.show_featured.description": "S e taba roghainneil a th ann an “Ga bhrosnachadh” far an urrainn dhut cunntasan eile a nochdadh.",
"account_edit.profile_tab.show_featured.title": "Seall an taba “Ga bhrosnachadh”",
"account_edit.profile_tab.show_media.description": "S e taba roghainneil a th ann am “Meadhanan” a sheallas na postaichean agad ris a bheil dealbh no video.",
"account_edit.profile_tab.show_media.title": "Seall an taba “Meadhanan”",
"account_edit.profile_tab.show_media_replies.description": "Nuair a bhios seo an comas, seallaidh an taba “Meadhanan” an dà chuid na postaichean agad agus na freagairtean a rinn thu do phostaichean càich.",
"account_edit.profile_tab.show_media_replies.title": "Gabh a-staigh freagairtean air an taba “Meadhanan”",
"account_edit.profile_tab.subtitle": "Gnàthaich na tabaichean air a phròifil agad is na sheallas iad.",
"account_edit.profile_tab.title": "Roghainnean tabaichean na pròifile",
"account_edit.save": "Sàbhail",
"account_edit.upload_modal.back": "Air ais",
"account_edit.upload_modal.done": "Deiseil",
"account_edit.upload_modal.next": "Air adhart",
"account_edit.upload_modal.step_crop.zoom": "Sùm",
"account_edit.upload_modal.step_upload.button": "Rùraich na faidhlichean",
"account_edit.upload_modal.step_upload.dragging": "Leig às airson luchdadh suas",
"account_edit.upload_modal.step_upload.header": "Tagh dealbh",
"account_edit.upload_modal.step_upload.hint": "Fòrmat WEBP, PNG, GIF no JPG, suas ri {limit}MB.{br}Thèid an dealbh a sgèileadh gu {width}x{height}px.",
"account_edit.upload_modal.title_add.avatar": "Cuir dealbh ris a phròifil",
"account_edit.upload_modal.title_add.header": "Cuir dealbh còmhdachaidh ris",
"account_edit.upload_modal.title_replace.avatar": "Cuir dealbh ùr an àite dealbh na pròifil",
"account_edit.upload_modal.title_replace.header": "Cuir dealbh ùr an àite an deilbh chòmhdachaidh",
"account_edit.verified_modal.details": "Cuir ri teistealachd do chunntais Mhastodon le dearbhadh cheanglaichean gu duilleagan-lìn parsanta. Seo mar a nì thu e:",
"account_edit.verified_modal.invisible_link.details": "Cuir an ceangal ris a bhann-chinn agad. S e rel=\"me\" a tha sa phàirt chudromach a bhacas riochd cuideigin eile air làraichean-lìn le susbaint air a gintinn o chleachdaiche. S urrainn dhut fiù taga link a chleachdadh ann am bann-cinn na duilleige seach {tag} ach feumaidh sinn an HTML ruigsinn gun a bhith a ruith JavaScript.",
"account_edit.verified_modal.invisible_link.summary": "Ciamar a dhfhalaicheas mi an ceangal?",
"account_edit.verified_modal.step1.header": "Dèan lethbhreac dhen chòd HTML gu h-ìosal is cuir e ri bann-cinn na làraich-lìn agad",
"account_edit.verified_modal.step2.details": "Ma chuir thu an làrach-lìn agad mar raon ghnàthaichte ris cheana, feumaidh tu a sguabadh às s a chur ris a-rithist airson an dearbhadh a chur gu dol.",
"account_edit.verified_modal.step2.header": "Cuir an làrach-lìn agad ris na raon gnàthaichte",
"account_edit.verified_modal.title": "Mar a chuireas tu ceangal dearbhte ris",
"account_edit_tags.add_tag": "Cuir #{tagName} ris",
"account_edit_tags.column_title": "Deasaich na tagaichean",
"account_edit_tags.help_text": "Cuidichidh na tagaichean hais brosnaichte gun lorg an luchd-cleachdaidh a phròifil agad s ach an dèan iad conaltradh ris. Nochdaidh iad nan criathragan air sealladh “Gnìomhachd” duilleag na pròifil agad.",
"account_edit_tags.max_tags_reached": "Ràinig thu na tha ceadaichte dhut de thagaichean hais brosnaichte.",
"account_edit_tags.search_placeholder": "Cuir a-steach taga hais…",
"account_edit_tags.suggestions": "Molaidhean:",
"account_edit_tags.tag_status_count": "{count, plural, one {# phost} two {# phost} few {# postaichean} other {# post}}",
"account_list.total": "{total, plural, one {# chunntas} two {# chunntas} few {# cunntasan} other {# cunntas}}",
"account_note.placeholder": "Briog airson nòta a chur ris", "account_note.placeholder": "Briog airson nòta a chur ris",
"admin.dashboard.daily_retention": "Reat glèidheadh nan cleachdaichean às dèidh an clàradh a-rèir latha", "admin.dashboard.daily_retention": "Reat glèidheadh nan cleachdaichean às dèidh an clàradh a-rèir latha",
"admin.dashboard.monthly_retention": "Reat glèidheadh nan cleachdaichean às dèidh an clàradh a-rèir mìos", "admin.dashboard.monthly_retention": "Reat glèidheadh nan cleachdaichean às dèidh an clàradh a-rèir mìos",
@ -184,16 +339,85 @@
"bundle_modal_error.close": "Dùin", "bundle_modal_error.close": "Dùin",
"bundle_modal_error.message": "Chaidh rudeigin ceàrr le luchdadh na sgrìn seo.", "bundle_modal_error.message": "Chaidh rudeigin ceàrr le luchdadh na sgrìn seo.",
"bundle_modal_error.retry": "Feuch ris a-rithist", "bundle_modal_error.retry": "Feuch ris a-rithist",
"callout.dismiss": "Leig seachad",
"carousel.current": "<sr>Sleamhnag</sr> {current, number} / {max, number}", "carousel.current": "<sr>Sleamhnag</sr> {current, number} / {max, number}",
"carousel.slide": "Sleamhnag {current, number} à {max, number}", "carousel.slide": "Sleamhnag {current, number} à {max, number}",
"character_counter.recommended": "{currentLength}/{maxLength} dhe na caractaran a mholamaid",
"character_counter.required": "{currentLength}/{maxLength} caractar(an)",
"closed_registrations.other_server_instructions": "Air sgàth s gu bheil Mastodon sgaoilte, s urrainn dhut cunntas a chruthachadh air frithealaiche eile agus conaltradh ris an fhrithealaiche seo co-dhiù.", "closed_registrations.other_server_instructions": "Air sgàth s gu bheil Mastodon sgaoilte, s urrainn dhut cunntas a chruthachadh air frithealaiche eile agus conaltradh ris an fhrithealaiche seo co-dhiù.",
"closed_registrations_modal.description": "Cha ghabh cunntas a chruthachadh air {domain} aig an àm seo ach thoir an aire nach fheum thu cunntas air {domain} gu sònraichte airson Mastodon a chleachdadh.", "closed_registrations_modal.description": "Cha ghabh cunntas a chruthachadh air {domain} aig an àm seo ach thoir an aire nach fheum thu cunntas air {domain} gu sònraichte airson Mastodon a chleachdadh.",
"closed_registrations_modal.find_another_server": "Lorg frithealaiche eile", "closed_registrations_modal.find_another_server": "Lorg frithealaiche eile",
"closed_registrations_modal.preamble": "Tha Mastodon sgaoilte is mar sin dheth ge b e càit an cruthaich thu an cunntas agad, s urrainn dhut duine sam bith a leantainn air an fhrithealaiche seo is conaltradh leotha. S urrainn dhut fiù s frithealaiche agad fhèin òstadh!", "closed_registrations_modal.preamble": "Tha Mastodon sgaoilte is mar sin dheth ge b e càit an cruthaich thu an cunntas agad, s urrainn dhut duine sam bith a leantainn air an fhrithealaiche seo is conaltradh leotha. S urrainn dhut fiù s frithealaiche agad fhèin òstadh!",
"closed_registrations_modal.title": "Clàradh le Mastodon", "closed_registrations_modal.title": "Clàradh le Mastodon",
"collection.share_modal.share_link_label": "Ceangal co-roinnidh",
"collection.share_modal.share_via_post": "Postaich air Mastodon",
"collection.share_modal.share_via_system": "Co-roinn gu…",
"collection.share_modal.title": "Co-roinn an cruinneachadh",
"collection.share_modal.title_new": "Co-roinn an cruinneachadh ùr agad!",
"collection.share_template_other": "Thoir sùil air an deagh-chruinneachadh seo: {link}",
"collection.share_template_own": "Thoir sùil air a chruinneachadh ùr agam: {link}",
"collections.account_count": "{count, plural, one {# chunntas} two {# chunntas} few {# cunntasan} other {# cunntas}}",
"collections.accounts.empty_description": "Cuir ris suas ri {count} cunntas(an) a tha thu a leantainn",
"collections.accounts.empty_title": "Tha an an cruinneachadh seo falamh",
"collections.by_account": "le {account_handle}",
"collections.collection_description": "Tuairisgeul",
"collections.collection_language": "Cànan",
"collections.collection_language_none": "Chan eil gin",
"collections.collection_name": "Ainm",
"collections.collection_topic": "Cuspair",
"collections.confirm_account_removal": "A bheil thu cinnteach gu bheil thu airson an cunntas seo a thoirt air falbh on chruinneachadh seo?",
"collections.content_warning": "Rabhadh susbainte",
"collections.continue": "Lean air adhart",
"collections.create.accounts_subtitle": "Chan urrainn dhut cur ris ach cunntasan a leanas tu s a ghabh ri rùrachadh.",
"collections.create.accounts_title": "Cò bhrosnaicheas tu sa chruinneachadh seo?",
"collections.create.basic_details_title": "Bun-fhiosrachadh",
"collections.create.steps": "Ceum {step}/{total}",
"collections.create_a_collection_hint": "Cruthaich cruinneachadh airson na cunntasan as fheàrr leat a mholadh no a cho-roinneadh le càch.",
"collections.create_collection": "Cruthaich cruinneachadh",
"collections.delete_collection": "Sguab an cruinneachadh às",
"collections.description_length_hint": "Crìoch de 100 caractar",
"collections.detail.accept_inclusion": "Taghta",
"collections.detail.accounts_heading": "Cunntasan",
"collections.detail.author_added_you": "Chuir {author} ris a chruinneachadh seo thu",
"collections.detail.curated_by_author": "Ga thasgadh le {author}",
"collections.detail.curated_by_you": "Ga thasgadh leatsa ",
"collections.detail.loading": "A luchdadh a chruinneachaidh…",
"collections.detail.other_accounts_in_collection": "Daoine eile sa chruinneachadh seo:",
"collections.detail.revoke_inclusion": "Thoir air falbh mi",
"collections.detail.sensitive_note": "Tha cunntasan is susbaint sa chruinneachadh seo a dhfhaodadh a bhith frionasach do chuid.",
"collections.detail.share": "Co-roinn an cruinneachadh seo",
"collections.edit_details": "Deasaich am fiosrachadh",
"collections.error_loading_collections": "Thachair mearachd nuair a dhfheuch sinn ris a chruinneachaidhean agad a luchdadh.",
"collections.hints.accounts_counter": "{count} / {max} cunntas(an)",
"collections.last_updated_at": "An tùrachadh mu dheireadh: {date}",
"collections.manage_accounts": "Stiùirich na cunntasan",
"collections.mark_as_sensitive": "Cuir comharra gu bheil e frionasach",
"collections.mark_as_sensitive_hint": "Falaichidh seo tuairisgeul is cunntasan a chruinneachaidh air cùlaibh rabhadh susbainte. Chithear ainm a chruinneachaidh fhathast.",
"collections.name_length_hint": "Crìoch de 40 caractar",
"collections.new_collection": "Cruinneachadh ùr",
"collections.no_collections_yet": "Chan eil cruinneachadh agad fhathast.",
"collections.old_last_post_note": "Tha am post mu dheireadh còrr is seachdain air ais",
"collections.remove_account": "Thoir air falbh an cunntas seo",
"collections.report_collection": "Dèan gearan mun chruinneachadh seo",
"collections.revoke_collection_inclusion": "Thoir mi fhìn air falbh on chruinneachadh seo",
"collections.revoke_inclusion.confirmation": "Chaidh do thoirt air falbh o “{collection}”",
"collections.revoke_inclusion.error": "Thachair mearachd. Feuch ris a-rithist an ceann greis.",
"collections.search_accounts_label": "Lorg cunntasan gus an cur ris…",
"collections.search_accounts_max_reached": "Chuir thu na tha ceadaichte de chunntasan ris",
"collections.sensitive": "Frionasach",
"collections.topic_hint": "Cuir taga hais ris a chuidicheas càch le tuigse prìomh-chuspair a chruinneachaidh seo.",
"collections.topic_special_chars_hint": "Thèid caractaran sònraichte a thoirt air falbh nuair a thèid a shàbhaladh",
"collections.view_collection": "Seall an cruinneachadh ",
"collections.view_other_collections_by_user": "Seall cruinneachaidhean eile aig a chleachdaiche seo",
"collections.visibility_public": "Poblach",
"collections.visibility_public_hint": "Gabhaidh a rùrachadh ann an toraidhean luirg agus air àitichean eile far a nochdas molaidhean.",
"collections.visibility_title": "Faicsinneachd",
"collections.visibility_unlisted": "Falaichte o liostaichean",
"collections.visibility_unlisted_hint": "Chì duine sam bith aig a bheil ceangal e. Thèid fhalach o thoraidhean luirg s na molaidhean.",
"column.about": "Mu dhèidhinn", "column.about": "Mu dhèidhinn",
"column.blocks": "Cleachdaichean bacte", "column.blocks": "Cleachdaichean bacte",
"column.bookmarks": "Comharran-lìn", "column.bookmarks": "Comharran-lìn",
"column.collections": "Na cruinneachaidhean agam",
"column.community": "Loidhne-ama ionadail", "column.community": "Loidhne-ama ionadail",
"column.create_list": "Cruthaich liosta", "column.create_list": "Cruthaich liosta",
"column.direct": "Iomraidhean prìobhaideach", "column.direct": "Iomraidhean prìobhaideach",
@ -220,6 +444,11 @@
"column_header.show_settings": "Seall na roghainnean", "column_header.show_settings": "Seall na roghainnean",
"column_header.unpin": "Dì-phrìnich", "column_header.unpin": "Dì-phrìnich",
"column_search.cancel": "Sguir dheth", "column_search.cancel": "Sguir dheth",
"combobox.close_results": "Dùin na toraidhean",
"combobox.loading": "Ga luchdadh",
"combobox.no_results_found": "Cha deach toradh a lorg",
"combobox.open_results": "Fosgail na toraidhean",
"combobox.results_available": "Tha {count, plural, one {# mholadh} two {# mholadh} few {# molaidhean } other {# moladh}} ri fhaighinn. Cleachd an t-saighead suas no sìos airson gluasad mun cuairt. Brùth air Enter airson taghadh.",
"community.column_settings.local_only": "Feadhainn ionadail a-mhàin", "community.column_settings.local_only": "Feadhainn ionadail a-mhàin",
"community.column_settings.media_only": "Meadhanan a-mhàin", "community.column_settings.media_only": "Meadhanan a-mhàin",
"community.column_settings.remote_only": "Feadhainn chèin a-mhàin", "community.column_settings.remote_only": "Feadhainn chèin a-mhàin",
@ -253,6 +482,9 @@
"confirmations.delete.confirm": "Sguab às", "confirmations.delete.confirm": "Sguab às",
"confirmations.delete.message": "A bheil thu cinnteach gu bheil thu airson am post seo a sguabadh às?", "confirmations.delete.message": "A bheil thu cinnteach gu bheil thu airson am post seo a sguabadh às?",
"confirmations.delete.title": "A bheil thu airson am post a sguabadh às?", "confirmations.delete.title": "A bheil thu airson am post a sguabadh às?",
"confirmations.delete_collection.confirm": "Sguab às",
"confirmations.delete_collection.message": "Cha ghabh seo a neo-dhèanamh.",
"confirmations.delete_collection.title": "A bheil thu airson “{name}” a sguabadh às?",
"confirmations.delete_list.confirm": "Sguab às", "confirmations.delete_list.confirm": "Sguab às",
"confirmations.delete_list.message": "A bheil thu cinnteach gu bheil thu airson an liosta seo a sguabadh às gu buan?", "confirmations.delete_list.message": "A bheil thu cinnteach gu bheil thu airson an liosta seo a sguabadh às gu buan?",
"confirmations.delete_list.title": "A bheil thu airson an liosta a sguabadh às?", "confirmations.delete_list.title": "A bheil thu airson an liosta a sguabadh às?",
@ -265,6 +497,9 @@
"confirmations.discard_draft.post.title": "A bheil thu airson dreachd a phuist agad a thilgeil air falbh?", "confirmations.discard_draft.post.title": "A bheil thu airson dreachd a phuist agad a thilgeil air falbh?",
"confirmations.discard_edit_media.confirm": "Tilg air falbh", "confirmations.discard_edit_media.confirm": "Tilg air falbh",
"confirmations.discard_edit_media.message": "Tha atharraichean gun sàbhaladh agad ann an tuairisgeul no ro-shealladh a mheadhain, a bheil thu airson an tilgeil air falbh co-dhiù?", "confirmations.discard_edit_media.message": "Tha atharraichean gun sàbhaladh agad ann an tuairisgeul no ro-shealladh a mheadhain, a bheil thu airson an tilgeil air falbh co-dhiù?",
"confirmations.follow_to_collection.confirm": "Lean s cuir ri cruinneachadh",
"confirmations.follow_to_collection.message": "Feumaidh tu {name} a leantainn mus cuir thu ri cruinneachadh iad.",
"confirmations.follow_to_collection.title": "A bheil thu airson an cunntas a leantainn?",
"confirmations.follow_to_list.confirm": "Lean s cuir ris an liosta", "confirmations.follow_to_list.confirm": "Lean s cuir ris an liosta",
"confirmations.follow_to_list.message": "Feumaidh tu {name} a leantainn ron chur ri liosta.", "confirmations.follow_to_list.message": "Feumaidh tu {name} a leantainn ron chur ri liosta.",
"confirmations.follow_to_list.title": "A bheil thu airson an cleachdaiche a leantainn?", "confirmations.follow_to_list.title": "A bheil thu airson an cleachdaiche a leantainn?",
@ -291,6 +526,9 @@
"confirmations.remove_from_followers.confirm": "Thoir an neach-leantainn air falbh", "confirmations.remove_from_followers.confirm": "Thoir an neach-leantainn air falbh",
"confirmations.remove_from_followers.message": "Cha lean {name} thu tuilleadh. A bheil thu cinnteach gu bheil thu airson leantainn air adhart?", "confirmations.remove_from_followers.message": "Cha lean {name} thu tuilleadh. A bheil thu cinnteach gu bheil thu airson leantainn air adhart?",
"confirmations.remove_from_followers.title": "A bheil thu airson an neach-leantainn a thoirt air falbh?", "confirmations.remove_from_followers.title": "A bheil thu airson an neach-leantainn a thoirt air falbh?",
"confirmations.revoke_collection_inclusion.confirm": "Thoir air falbh mi",
"confirmations.revoke_collection_inclusion.message": "Tha an gnìomh seo buan agus chan urrainn dhan neach-tasgaidh do chur ris a chruinneachadh a-rithist an uair sin.",
"confirmations.revoke_collection_inclusion.title": "A bheil tu airson do thoirt air falbh on chruinneachadh seo?",
"confirmations.revoke_quote.confirm": "Thoir am post air falbh", "confirmations.revoke_quote.confirm": "Thoir am post air falbh",
"confirmations.revoke_quote.message": "Cha ghabh seo a neo-dhèanamh.", "confirmations.revoke_quote.message": "Cha ghabh seo a neo-dhèanamh.",
"confirmations.revoke_quote.title": "A bheil thu airson am post a thoirt air falbh?", "confirmations.revoke_quote.title": "A bheil thu airson am post a thoirt air falbh?",
@ -308,6 +546,7 @@
"conversation.open": "Seall an còmhradh", "conversation.open": "Seall an còmhradh",
"conversation.with": "Còmhla ri {names}", "conversation.with": "Còmhla ri {names}",
"copy_icon_button.copied": "Chaidh lethbhreac dheth a chur air an stòr-bhòrd", "copy_icon_button.copied": "Chaidh lethbhreac dheth a chur air an stòr-bhòrd",
"copy_icon_button.copy_this_text": "Cuir lethbhreac dhen cheangal air an stòr-bhòrd",
"copypaste.copied": "Chaidh lethbhreac dheth a dhèanamh", "copypaste.copied": "Chaidh lethbhreac dheth a dhèanamh",
"copypaste.copy_to_clipboard": "Cuir lethbhreac dheth air an stòr-bhòrd", "copypaste.copy_to_clipboard": "Cuir lethbhreac dheth air an stòr-bhòrd",
"directory.federated": "On cho-shaoghal aithnichte", "directory.federated": "On cho-shaoghal aithnichte",
@ -384,6 +623,7 @@
"empty_column.notification_requests": "Glan! Chan eil dad an-seo. Nuair a gheibh thu brathan ùra, nochdaidh iad an-seo a-rèir nan roghainnean agad.", "empty_column.notification_requests": "Glan! Chan eil dad an-seo. Nuair a gheibh thu brathan ùra, nochdaidh iad an-seo a-rèir nan roghainnean agad.",
"empty_column.notifications": "Cha d fhuair thu brath sam bith fhathast. Nuair a nì càch conaltradh leat, chì thu an-seo e.", "empty_column.notifications": "Cha d fhuair thu brath sam bith fhathast. Nuair a nì càch conaltradh leat, chì thu an-seo e.",
"empty_column.public": "Chan eil dad an-seo! Sgrìobh rudeigin gu poblach no lean càch o fhrithealaichean eile a làimh airson seo a lìonadh", "empty_column.public": "Chan eil dad an-seo! Sgrìobh rudeigin gu poblach no lean càch o fhrithealaichean eile a làimh airson seo a lìonadh",
"empty_state.no_results": "Gun toradh",
"error.no_hashtag_feed_access": "Cruthaich cunntas no clàraich a-steach airson an taga hais seo a shealltainn is leantainn.", "error.no_hashtag_feed_access": "Cruthaich cunntas no clàraich a-steach airson an taga hais seo a shealltainn is leantainn.",
"error.unexpected_crash.explanation": "Air sàilleibh buga sa chòd againn no duilgheadas co-chòrdalachd leis a bhrabhsair, chan urrainn dhuinn an duilleag seo a shealltainn mar bu chòir.", "error.unexpected_crash.explanation": "Air sàilleibh buga sa chòd againn no duilgheadas co-chòrdalachd leis a bhrabhsair, chan urrainn dhuinn an duilleag seo a shealltainn mar bu chòir.",
"error.unexpected_crash.explanation_addons": "Cha b urrainn dhuinn an duilleag seo a shealltainn mar bu chòir. Tha sinn an dùil gu do dhadhbharaich tuilleadan a bhrabhsair no inneal eadar-theangachaidh fèin-obrachail a mhearachd.", "error.unexpected_crash.explanation_addons": "Cha b urrainn dhuinn an duilleag seo a shealltainn mar bu chòir. Tha sinn an dùil gu do dhadhbharaich tuilleadan a bhrabhsair no inneal eadar-theangachaidh fèin-obrachail a mhearachd.",
@ -399,6 +639,11 @@
"featured_carousel.current": "<sr>Post</sr> {current, number} / {max, number}", "featured_carousel.current": "<sr>Post</sr> {current, number} / {max, number}",
"featured_carousel.header": "{count, plural, one {Post prìnichte} two {Postaichean prìnichte} few {Postaichean prìnichte} other {Postaichean prìnichte}}", "featured_carousel.header": "{count, plural, one {Post prìnichte} two {Postaichean prìnichte} few {Postaichean prìnichte} other {Postaichean prìnichte}}",
"featured_carousel.slide": "Post {current, number} à {max, number}", "featured_carousel.slide": "Post {current, number} à {max, number}",
"featured_tags.more_items": "+{count}",
"featured_tags.suggestions": "Sgrìobh thu mu {items} o chionn goirid. A bheil thu airson an cur ris nan tagaichean hais brosnaichte?",
"featured_tags.suggestions.add": "Cuir ris",
"featured_tags.suggestions.added": "Stiùirich na tagaichean hais brosnaichte agad uair sam bith aig <link>Deasaich a phròifil > Tagaichean hais brosnaichte</link>.",
"featured_tags.suggestions.dismiss": "Na cuir ris",
"filter_modal.added.context_mismatch_explanation": "Chan eil an roinn-seòrsa criathraidh iom seo chaidh dhan cho-theacs san do dhinntrig thu am post seo. Ma tha thu airson am post a chriathradh sa cho-theacs seo cuideachd, feumaidh tu a chriathrag a dheasachadh.", "filter_modal.added.context_mismatch_explanation": "Chan eil an roinn-seòrsa criathraidh iom seo chaidh dhan cho-theacs san do dhinntrig thu am post seo. Ma tha thu airson am post a chriathradh sa cho-theacs seo cuideachd, feumaidh tu a chriathrag a dheasachadh.",
"filter_modal.added.context_mismatch_title": "Co-theacsa neo-iomchaidh!", "filter_modal.added.context_mismatch_title": "Co-theacsa neo-iomchaidh!",
"filter_modal.added.expired_explanation": "Dhfhalbh an ùine air an roinn-seòrsa criathraidh seo agus feumaidh tu an ceann-là crìochnachaidh atharrachadh mus cuir thu an sàs i.", "filter_modal.added.expired_explanation": "Dhfhalbh an ùine air an roinn-seòrsa criathraidh seo agus feumaidh tu an ceann-là crìochnachaidh atharrachadh mus cuir thu an sàs i.",
@ -440,6 +685,10 @@
"follow_suggestions.view_all": "Seall na h-uile", "follow_suggestions.view_all": "Seall na h-uile",
"follow_suggestions.who_to_follow": "Molaidhean leantainn", "follow_suggestions.who_to_follow": "Molaidhean leantainn",
"followed_tags": "Tagaichean hais gan leantainn", "followed_tags": "Tagaichean hais gan leantainn",
"followers.hide_other_followers": "Chuir an cleachdaiche seo roimhe nach fhaicear an luchd-leantainn eile aca",
"followers.title": "A leantainn {name}",
"following.hide_other_following": "Chuir an cleachdaiche seo roimhe nach fhaicear cò eile a leanas iad",
"following.title": "Ga leantainn le {name}",
"footer.about": "Mu dhèidhinn", "footer.about": "Mu dhèidhinn",
"footer.about_mastodon": "Mu Mhastodon", "footer.about_mastodon": "Mu Mhastodon",
"footer.about_server": "Mu {domain}", "footer.about_server": "Mu {domain}",
@ -451,6 +700,8 @@
"footer.source_code": "Seall am bun-tùs", "footer.source_code": "Seall am bun-tùs",
"footer.status": "Staid", "footer.status": "Staid",
"footer.terms_of_service": "Teirmichean na seirbheise", "footer.terms_of_service": "Teirmichean na seirbheise",
"form_error.blank": "Chan fhaod an raon a bhith bàn.",
"form_field.optional": "(roghainneil)",
"generic.saved": "Chaidh a shàbhaladh", "generic.saved": "Chaidh a shàbhaladh",
"getting_started.heading": "Toiseach", "getting_started.heading": "Toiseach",
"hashtag.admin_moderation": "Fosgail eadar-aghaidh na maorsainneachd dha #{name}", "hashtag.admin_moderation": "Fosgail eadar-aghaidh na maorsainneachd dha #{name}",
@ -520,6 +771,7 @@
"keyboard_shortcuts.direct": "Fosgail colbh nan iomraidhean prìobhaideach", "keyboard_shortcuts.direct": "Fosgail colbh nan iomraidhean prìobhaideach",
"keyboard_shortcuts.down": "Gluais sìos air an liosta", "keyboard_shortcuts.down": "Gluais sìos air an liosta",
"keyboard_shortcuts.enter": "Fosgail post", "keyboard_shortcuts.enter": "Fosgail post",
"keyboard_shortcuts.explore": "Fosgail na treandaichean",
"keyboard_shortcuts.favourite": "Cuir am post ris na h-annsachdan", "keyboard_shortcuts.favourite": "Cuir am post ris na h-annsachdan",
"keyboard_shortcuts.favourites": "Fosgail liosta nan annsachdan", "keyboard_shortcuts.favourites": "Fosgail liosta nan annsachdan",
"keyboard_shortcuts.federated": "Fosgail an loidhne-ama cho-naisgte", "keyboard_shortcuts.federated": "Fosgail an loidhne-ama cho-naisgte",
@ -606,6 +858,7 @@
"navigation_bar.automated_deletion": "Sguabadh às phostaichean", "navigation_bar.automated_deletion": "Sguabadh às phostaichean",
"navigation_bar.blocks": "Cleachdaichean bacte", "navigation_bar.blocks": "Cleachdaichean bacte",
"navigation_bar.bookmarks": "Comharran-lìn", "navigation_bar.bookmarks": "Comharran-lìn",
"navigation_bar.collections": "Cruinneachaidhean",
"navigation_bar.direct": "Iomraidhean prìobhaideach", "navigation_bar.direct": "Iomraidhean prìobhaideach",
"navigation_bar.domain_blocks": "Àrainnean bacte", "navigation_bar.domain_blocks": "Àrainnean bacte",
"navigation_bar.favourites": "Annsachdan", "navigation_bar.favourites": "Annsachdan",
@ -753,18 +1006,20 @@
"notifications_permission_banner.title": "Na caill dad gu bràth tuilleadh", "notifications_permission_banner.title": "Na caill dad gu bràth tuilleadh",
"onboarding.follows.back": "Air ais", "onboarding.follows.back": "Air ais",
"onboarding.follows.empty": "Gu mì-fhortanach, chan urrainn dhuinn toradh a shealltainn an-dràsta. Feuch gleus an luirg no duilleag an rùrachaidh airson daoine ri leantainn a lorg no feuch ris a-rithist an ceann tamaill.", "onboarding.follows.empty": "Gu mì-fhortanach, chan urrainn dhuinn toradh a shealltainn an-dràsta. Feuch gleus an luirg no duilleag an rùrachaidh airson daoine ri leantainn a lorg no feuch ris a-rithist an ceann tamaill.",
"onboarding.follows.next": "Air adhart: Suidhich a phròifil agad",
"onboarding.follows.search": "Lorg", "onboarding.follows.search": "Lorg",
"onboarding.follows.title": "Lean daoine airson tòiseachadh", "onboarding.follows.title": "Lean daoine airson tòiseachadh",
"onboarding.profile.discoverable": "Bu mhath leam gun gabh a phròifil agam a rùrachadh", "onboarding.profile.discoverable": "Bu mhath leam gun gabh a phròifil agam a rùrachadh",
"onboarding.profile.discoverable_hint": "Ma chuir thu romhad gun gabh a phròifil agad a rùrachadh air Mastodon, faodaidh na postaichean agad nochdadh ann an toraidhean luirg agus treandaichean agus dhfhaoidte gun dèid a phròifil agad a mholadh dhan fheadhainn aig a bheil ùidhean coltach ri d ùidhean-sa.", "onboarding.profile.discoverable_hint": "Ma chuir thu romhad gun gabh a phròifil agad a rùrachadh air Mastodon, faodaidh na postaichean agad nochdadh ann an toraidhean luirg agus treandaichean agus dhfhaoidte gun dèid a phròifil agad a mholadh dhan fheadhainn aig a bheil ùidhean coltach ri d ùidhean-sa.",
"onboarding.profile.display_name": "Ainm-taisbeanaidh", "onboarding.profile.display_name": "Ainm-taisbeanaidh",
"onboarding.profile.display_name_hint": "D ainm slàn no spòrsail…", "onboarding.profile.display_name_hint": "D ainm slàn no spòrsail…",
"onboarding.profile.finish": "Crìochnaich",
"onboarding.profile.note": "Cunntas-beatha", "onboarding.profile.note": "Cunntas-beatha",
"onboarding.profile.note_hint": "S urrainn dhut @iomradh a thoirt air càch no air #tagaicheanHais…", "onboarding.profile.note_hint": "S urrainn dhut @iomradh a thoirt air càch no air #tagaicheanHais…",
"onboarding.profile.title": "Suidheachadh na pròifile", "onboarding.profile.title": "Suidheachadh na pròifile",
"onboarding.profile.upload_avatar": "Luchdaich suas dealbh na pròifil", "onboarding.profile.upload_avatar": "Luchdaich suas dealbh na pròifil",
"onboarding.profile.upload_header": "Luchdaich suas bann-cinn na pròifil", "onboarding.profile.upload_header": "Luchdaich suas bann-cinn na pròifil",
"password_confirmation.exceeds_maxlength": "Tha dearbhadh an fhacail-fhaire nas fhaide na tha ceadaichte do dhfaclan-faire", "password_confirmation.exceeds_maxlength": "Tha dearbhadh an fhacail-fhaire nas fhaide na tha ceadaichte do dhfhaclan-faire",
"password_confirmation.mismatching": "Chan eil an dearbhadh co-ionnan ris an fhacal-fhaire", "password_confirmation.mismatching": "Chan eil an dearbhadh co-ionnan ris an fhacal-fhaire",
"picture_in_picture.restore": "Thoir air ais e", "picture_in_picture.restore": "Thoir air ais e",
"poll.closed": "Dùinte", "poll.closed": "Dùinte",
@ -784,6 +1039,7 @@
"privacy.private.short": "Luchd-leantainn", "privacy.private.short": "Luchd-leantainn",
"privacy.public.long": "Duine sam bith taobh a-staigh no a-muigh Mhastodon", "privacy.public.long": "Duine sam bith taobh a-staigh no a-muigh Mhastodon",
"privacy.public.short": "Poblach", "privacy.public.short": "Poblach",
"privacy.quote.anyone": "{visibility}, luaidh ceadaichte",
"privacy.quote.disabled": "{visibility}, luaidh à comas", "privacy.quote.disabled": "{visibility}, luaidh à comas",
"privacy.quote.limited": "{visibility}, luaidh cuingichte", "privacy.quote.limited": "{visibility}, luaidh cuingichte",
"privacy.unlisted.additional": "Tha seo coltach ris an fhaicsinneachd phoblach ach cha nochd am post air loidhnichean-ama an t-saoghail phoblaich, nan tagaichean hais no an rùrachaidh no ann an toraidhean luirg Mhastodon fiù s ma thug thu ro-aonta airson sin seachad.", "privacy.unlisted.additional": "Tha seo coltach ris an fhaicsinneachd phoblach ach cha nochd am post air loidhnichean-ama an t-saoghail phoblaich, nan tagaichean hais no an rùrachaidh no ann an toraidhean luirg Mhastodon fiù s ma thug thu ro-aonta airson sin seachad.",
@ -829,6 +1085,7 @@
"report.category.title_account": "phròifil", "report.category.title_account": "phròifil",
"report.category.title_status": "phost", "report.category.title_status": "phost",
"report.close": "Deiseil", "report.close": "Deiseil",
"report.collection_comment": "Carson a tha thu airson gearan a dhèanamh mun chruinneachadh seo?",
"report.comment.title": "A bheil rud sam bith eile a bu toigh leat innse dhuinn?", "report.comment.title": "A bheil rud sam bith eile a bu toigh leat innse dhuinn?",
"report.forward": "Sìn air adhart gu {target}", "report.forward": "Sìn air adhart gu {target}",
"report.forward_hint": "Chaidh an cunntas a chlàradh air frithealaiche eile. A bheil thu airson lethbhreac dhen ghearan a chur dha-san gun ainm cuideachd?", "report.forward_hint": "Chaidh an cunntas a chlàradh air frithealaiche eile. A bheil thu airson lethbhreac dhen ghearan a chur dha-san gun ainm cuideachd?",
@ -850,6 +1107,8 @@
"report.rules.title": "Dè na riaghailtean a tha gam briseadh?", "report.rules.title": "Dè na riaghailtean a tha gam briseadh?",
"report.statuses.subtitle": "Tagh a h-uile gin a tha iomchaidh", "report.statuses.subtitle": "Tagh a h-uile gin a tha iomchaidh",
"report.statuses.title": "A bheil postaichean sam bith ann a tha nam fianais dhan ghearan seo?", "report.statuses.title": "A bheil postaichean sam bith ann a tha nam fianais dhan ghearan seo?",
"report.submission_error": "Cha b urrainn dhuinn an gearan a chur a-null",
"report.submission_error_details": "Thoir sùil air a cheangal ris an lìonra agad is feuch ris a-rithist an ceann greis.",
"report.submit": "Cuir a-null", "report.submit": "Cuir a-null",
"report.target": "A gearan mu {target}", "report.target": "A gearan mu {target}",
"report.thanks.take_action": "Seo na roghainnean a th agad airson stiùireadh na chì thu air Mastodon:", "report.thanks.take_action": "Seo na roghainnean a th agad airson stiùireadh na chì thu air Mastodon:",
@ -903,6 +1162,9 @@
"sign_in_banner.mastodon_is": "Is Mastodon an dòigh as fheàrr airson sùil a chumail air na tha a dol.", "sign_in_banner.mastodon_is": "Is Mastodon an dòigh as fheàrr airson sùil a chumail air na tha a dol.",
"sign_in_banner.sign_in": "Clàraich a-steach", "sign_in_banner.sign_in": "Clàraich a-steach",
"sign_in_banner.sso_redirect": "Clàraich a-steach no clàraich leinn", "sign_in_banner.sso_redirect": "Clàraich a-steach no clàraich leinn",
"skip_links.hotkey": "<span>Grad-iuchair</span> {hotkey}",
"skip_links.skip_to_content": "Geàrr leum chun na prìomh-shusbainte",
"skip_links.skip_to_navigation": "Geàrr leum chun na prìomh-sheòladaireachd",
"status.admin_account": "Fosgail eadar-aghaidh na maorsainneachd dha @{name}", "status.admin_account": "Fosgail eadar-aghaidh na maorsainneachd dha @{name}",
"status.admin_domain": "Fosgail eadar-aghaidh na maorsainneachd dha {domain}", "status.admin_domain": "Fosgail eadar-aghaidh na maorsainneachd dha {domain}",
"status.admin_status": "Fosgail am post seo ann an eadar-aghaidh na maorsainneachd", "status.admin_status": "Fosgail am post seo ann an eadar-aghaidh na maorsainneachd",
@ -1004,6 +1266,7 @@
"tabs_bar.notifications": "Brathan", "tabs_bar.notifications": "Brathan",
"tabs_bar.publish": "Post ùr", "tabs_bar.publish": "Post ùr",
"tabs_bar.search": "Lorg", "tabs_bar.search": "Lorg",
"tag.remove": "Thoir air falbh",
"terms_of_service.effective_as_of": "Èifeachdach on {date}", "terms_of_service.effective_as_of": "Èifeachdach on {date}",
"terms_of_service.title": "Teirmichean na seirbheise", "terms_of_service.title": "Teirmichean na seirbheise",
"terms_of_service.upcoming_changes_on": "Tha atharraichean ri thighinn air {date}", "terms_of_service.upcoming_changes_on": "Tha atharraichean ri thighinn air {date}",

View File

@ -1,6 +1,7 @@
{ {
"about.blocks": "Jerata servili", "about.blocks": "Jerata servili",
"about.contact": "Kontaktajo:", "about.contact": "Kontaktajo:",
"about.default_locale": "Predeterminita",
"about.disclaimer": "Mastodon esas libera, publikfonta e komercmarko di Mastodon gGmbH.", "about.disclaimer": "Mastodon esas libera, publikfonta e komercmarko di Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Expliko nedisponebla", "about.domain_blocks.no_reason_available": "Expliko nedisponebla",
"about.domain_blocks.preamble": "Mastodon generale permisas on vidar kontenajo e interagar kun uzanti de irga altra servilo en fediverso. Existas eceptioni quo facesis che ca partikulara servilo.", "about.domain_blocks.preamble": "Mastodon generale permisas on vidar kontenajo e interagar kun uzanti de irga altra servilo en fediverso. Existas eceptioni quo facesis che ca partikulara servilo.",
@ -13,30 +14,55 @@
"about.powered_by": "Necentraligita sociala ret quo povigesas da {mastodon}", "about.powered_by": "Necentraligita sociala ret quo povigesas da {mastodon}",
"about.rules": "Servilreguli", "about.rules": "Servilreguli",
"account.account_note_header": "Personala noto", "account.account_note_header": "Personala noto",
"account.activity": "Ago",
"account.add_note": "Adjuntez personala noto",
"account.add_or_remove_from_list": "Adjuntar o forigar de listi", "account.add_or_remove_from_list": "Adjuntar o forigar de listi",
"account.badges.admin": "Administranto",
"account.badges.blocked": "Blokusita",
"account.badges.bot": "Boto", "account.badges.bot": "Boto",
"account.badges.domain_blocked": "Blokusita domeno",
"account.badges.group": "Grupo", "account.badges.group": "Grupo",
"account.badges.muted": "Silencigata",
"account.badges.muted_until": "Silencigita til {until}",
"account.block": "Blokusar @{name}", "account.block": "Blokusar @{name}",
"account.block_domain": "Blokusar {domain}", "account.block_domain": "Blokusar {domain}",
"account.block_short": "Blokusar", "account.block_short": "Blokusar",
"account.blocked": "Blokusita", "account.blocked": "Blokusita",
"account.blocking": "Blokusas",
"account.cancel_follow_request": "Desendez sequodemando", "account.cancel_follow_request": "Desendez sequodemando",
"account.copy": "Kopiez ligilo al profilo", "account.copy": "Kopiez ligilo al profilo",
"account.direct": "Private mencionez @{name}", "account.direct": "Private mencionez @{name}",
"account.disable_notifications": "Cesez avizar me kande @{name} postas", "account.disable_notifications": "Cesez avizar me kande @{name} postas",
"account.domain_blocking": "Blokusas domeno",
"account.edit_note": "Redaktez personala noto",
"account.edit_profile": "Redaktar profilo", "account.edit_profile": "Redaktar profilo",
"account.edit_profile_short": "Redaktez",
"account.enable_notifications": "Avizez me kande @{name} postas", "account.enable_notifications": "Avizez me kande @{name} postas",
"account.endorse": "Traito di profilo", "account.endorse": "Traito di profilo",
"account.featured": "Estalita",
"account.featured.accounts": "Profili",
"account.featured.collections": "Kolektaji",
"account.featured.hashtags": "Gretvorti",
"account.featured_tags.last_status_at": "Antea posto ye {date}", "account.featured_tags.last_status_at": "Antea posto ye {date}",
"account.featured_tags.last_status_never": "Nula posti", "account.featured_tags.last_status_never": "Nula posti",
"account.filters.all": "Omna ago",
"account.filters.boosts_toggle": "Montrez diskonoci",
"account.filters.posts_boosts": "Afishi e diskonoci",
"account.filters.posts_only": "Afishi",
"account.filters.posts_replies": "Afishi e respondi",
"account.filters.replies_toggle": "Montrez respondi",
"account.follow": "Sequar", "account.follow": "Sequar",
"account.follow_back": "Anke sequez", "account.follow_back": "Anke sequez",
"account.follow_request_cancel": "Anulez demando",
"account.follow_request_cancel_short": "Anulez",
"account.follow_request_short": "Demandez",
"account.followers": "Sequanti", "account.followers": "Sequanti",
"account.followers.empty": "Nulu sequas ca uzanto til nun.", "account.followers.empty": "Nulu sequas ca uzanto til nun.",
"account.followers_counter": "{count, plural,one {{counter} sequanto} other {{counter} sequanti}}", "account.followers_counter": "{count, plural,one {{counter} sequanto} other {{counter} sequanti}}",
"account.following": "Sequata", "account.following": "Sequata",
"account.following_counter": "{count, plural,one {{counter} sequato} other {{counter} sequati}}", "account.following_counter": "{count, plural,one {{counter} sequato} other {{counter} sequati}}",
"account.follows.empty": "Ca uzanto ne sequa irgu til nun.", "account.follows.empty": "Ca uzanto ne sequa irgu til nun.",
"account.follows_you": "Sequas vu",
"account.go_to_profile": "Irez al profilo", "account.go_to_profile": "Irez al profilo",
"account.hide_reblogs": "Celez repeti de @{name}", "account.hide_reblogs": "Celez repeti de @{name}",
"account.in_memoriam": "Memorige.", "account.in_memoriam": "Memorige.",
@ -46,12 +72,29 @@
"account.locked_info": "La privatesostaco di ca konto fixesas quale lokata. Proprietato manue kontrolas personi qui povas sequar.", "account.locked_info": "La privatesostaco di ca konto fixesas quale lokata. Proprietato manue kontrolas personi qui povas sequar.",
"account.media": "Audvidaji", "account.media": "Audvidaji",
"account.mention": "Mencionar @{name}", "account.mention": "Mencionar @{name}",
"account.menu.add_to_list": "Adjuntez ad listo…",
"account.menu.block": "Blokusez konto",
"account.menu.block_domain": "Blokuez {domain}",
"account.menu.direct": "Private mencionez",
"account.menu.hide_reblogs": "Celez diskonoci en tempolineo",
"account.menu.mention": "Mencionez",
"account.menu.mute": "Silencigez konto",
"account.menu.open_original_page": "Videz che {domain}",
"account.menu.remove_follower": "Efacez sequanto",
"account.menu.report": "Efacez konto",
"account.menu.share": "Kunhavigez…", "account.menu.share": "Kunhavigez…",
"account.menu.show_reblogs": "Montrez diskonoci en tempolineo",
"account.menu.unblock": "Retroblokusez konto",
"account.menu.unblock_domain": "Retroblokusez {domain}",
"account.menu.unmute": "Retrosilencigez konto",
"account.moved_to": "{name} indikis ke lua nova konto es nune:", "account.moved_to": "{name} indikis ke lua nova konto es nune:",
"account.mute": "Celar @{name}", "account.mute": "Celar @{name}",
"account.mute_notifications_short": "Silencigez avizi", "account.mute_notifications_short": "Silencigez avizi",
"account.mute_short": "Silencigez", "account.mute_short": "Silencigez",
"account.muted": "Silencigata", "account.muted": "Silencigata",
"account.muting": "Silencigas",
"account.name.help.footer": "Samkam vu povas sendar retposti ad personi per dessama retpostosoftwari, vu povas interagar kun personi che altra Mastodon-servili (ActivityPub-kodexaro).",
"account.name.help.header": "Nometo esas kam retpostadreso",
"account.no_bio": "Deskriptajo ne provizesis.", "account.no_bio": "Deskriptajo ne provizesis.",
"account.open_original_page": "Apertez originala pagino", "account.open_original_page": "Apertez originala pagino",
"account.posts": "Mesaji", "account.posts": "Mesaji",
@ -64,12 +107,38 @@
"account.unblock": "Desblokusar @{name}", "account.unblock": "Desblokusar @{name}",
"account.unblock_domain": "Desblokusar {domain}", "account.unblock_domain": "Desblokusar {domain}",
"account.unblock_short": "Desblokusar", "account.unblock_short": "Desblokusar",
"account.unendorse": "Ne publikigez che profilo", "account.unendorse": "Ne estalez che profilo",
"account.unfollow": "Ne plus sequar", "account.unfollow": "Ne plus sequar",
"account.unmute": "Ne plus celar @{name}", "account.unmute": "Ne plus celar @{name}",
"account.unmute_notifications_short": "Dessilencigez avizi", "account.unmute_notifications_short": "Dessilencigez avizi",
"account.unmute_short": "Desilencigez", "account.unmute_short": "Desilencigez",
"account_edit.custom_fields.placeholder": "Adjuntez vua pronomi, externa ligili o altra irgo quan vu volas kunhavigar.", "account_edit.custom_fields.placeholder": "Adjuntez vua pronomi, externa ligili o altra irgo quan vu volas kunhavigar.",
"account_edit.display_name.add_label": "Adjuntez nometo",
"account_edit.display_name.edit_label": "Redaktez nometo",
"account_edit.display_name.placeholder": "Vua nometo esas quale vua nomo aparas che vua profilo e en tempolinei.",
"account_edit.display_name.title": "Nometo",
"account_edit.featured_hashtags.edit_label": "Adjuntez gretvorti",
"account_edit.featured_hashtags.placeholder": "Helpez altru identifikar e havez rapida aceso por vua prizita topiki.",
"account_edit.featured_hashtags.title": "Estalita gretvorti",
"account_edit.field_edit_modal.length_warning": "Rekomendita literlimito esis ecesita.",
"account_edit.field_edit_modal.link_emoji_warning": "Ni desrekomendas uzar kustuma emoji kun url.",
"account_edit.field_edit_modal.url_warning": "Por adjuntar ligilo, inkluzez {protocol} che la komenco.",
"account_edit.name_modal.add_title": "Adjuntez nometo",
"account_edit.name_modal.edit_title": "Redaktez nometo",
"account_edit.profile_tab.button_label": "Kustumizez",
"account_edit.profile_tab.hint.description": "Ca preferaji kustumizas quon uzanti vidas che {server} per oficala softwari, me li forsan ne esas sama ad uzanti che altra servili e desoficala softwari.",
"account_edit.profile_tab.hint.title": "Vidi ankore esas chanjita",
"account_edit.profile_tab.show_featured.description": "'Estalita' esas nemusta langeto ube vu povas dismontrar altra konti.",
"account_edit.profile_tab.show_featured.title": "Montrez 'Estalita'-langeto",
"account_edit.profile_tab.show_media.description": "'Audvidajo' esas desmusta langeto qua montras vua posti qui kontenas imaji o videii.",
"account_edit.profile_tab.show_media.title": "Montrez 'Audvidajo'-langeto",
"account_edit.profile_tab.show_media_replies.description": "Kande aktivigita, audvidajlangeto montras ambe vua afishi e respondi ad afishi di altra personi.",
"account_edit.profile_tab.show_media_replies.title": "Inkluzas respondi che 'Audvidajo'-langeto",
"account_edit.profile_tab.subtitle": "Kustumizas la langeti che vua profilo e quon li montras.",
"account_edit.profile_tab.title": "Profillangetpreferaji",
"account_edit.verified_modal.invisible_link.details": "Adjuntez la ligilo ad vua kapimajo. Vu anke povas uzar ligiletiketo en la kapimajo di la pagino vice {tag}, ma la HTML mustas esar acesebla sen exekutar JavaScript.",
"account_edit_tags.help_text": "Estalita gretvorti helpas uzanti deskovrar e interagar kun vua profilo.",
"account_edit_tags.max_tags_reached": "Vu atingas la maxim quanto di estalita gretvorti.",
"account_note.placeholder": "Klikez por adjuntar noto", "account_note.placeholder": "Klikez por adjuntar noto",
"admin.dashboard.daily_retention": "Dia uzantoretenseso pos registro", "admin.dashboard.daily_retention": "Dia uzantoretenseso pos registro",
"admin.dashboard.monthly_retention": "Monata uzantoreteneso pos registro", "admin.dashboard.monthly_retention": "Monata uzantoreteneso pos registro",
@ -96,6 +165,12 @@
"annual_report.shared_page.donate": "Donacez", "annual_report.shared_page.donate": "Donacez",
"annual_report.shared_page.footer": "Igita kun {heart} da la grupo di Mastodon", "annual_report.shared_page.footer": "Igita kun {heart} da la grupo di Mastodon",
"annual_report.shared_page.footer_server_info": "{username} uzas {domain}, un de multa komunitati quin povizita da Mastodon.", "annual_report.shared_page.footer_server_info": "{username} uzas {domain}, un de multa komunitati quin povizita da Mastodon.",
"annual_report.summary.archetype.booster.desc_public": "{name} dauras serchar afishi por diskonoci e pluigas altra kreanti.",
"annual_report.summary.archetype.booster.desc_self": "Vu dauras serchar afishi por diskonoci e pluigas altra kreanti.",
"annual_report.summary.archetype.booster.name": "La arkisto",
"annual_report.summary.archetype.lurker.desc_public": "Ni povas ke {name} esas ulaloke e juas Mastodon.",
"annual_report.summary.archetype.lurker.desc_self": "Ni savas ke vu esas ulaloke e juas Mastodon.",
"annual_report.summary.highlighted_post.boost_count": "Ca afisho esis diskonocita {count, plural,one {unfoye} other {# foye}}.",
"annual_report.summary.most_used_app.most_used_app": "maxim uzita aplikajo", "annual_report.summary.most_used_app.most_used_app": "maxim uzita aplikajo",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "maxim uzita gretvorto", "annual_report.summary.most_used_hashtag.most_used_hashtag": "maxim uzita gretvorto",
"annual_report.summary.new_posts.new_posts": "nova afishi", "annual_report.summary.new_posts.new_posts": "nova afishi",
@ -141,8 +216,11 @@
"collection.share_modal.title_new": "Kunhavigez vua nova kolektajo!", "collection.share_modal.title_new": "Kunhavigez vua nova kolektajo!",
"collection.share_template_other": "Videz ca splendida kolektajo: {link}", "collection.share_template_other": "Videz ca splendida kolektajo: {link}",
"collection.share_template_own": "Videz mia nova kolektajo: {link}", "collection.share_template_own": "Videz mia nova kolektajo: {link}",
"collections.content_warning": "Kontenajaverto",
"collections.create.accounts_title": "Quan vu estalos en ca kolektajo?",
"collections.create_a_collection_hint": "Kreez kolektajo por rekomendar o kunhavigez vua favoriza konti kun altri.", "collections.create_a_collection_hint": "Kreez kolektajo por rekomendar o kunhavigez vua favoriza konti kun altri.",
"collections.detail.share": "Kunhavigez ca kolektajo", "collections.detail.share": "Kunhavigez ca kolektajo",
"collections.mark_as_sensitive_hint": "Celas deskripto di la kolektajo e konti dop kontenajaverto.",
"column.about": "Pri co", "column.about": "Pri co",
"column.blocks": "Blokusita uzeri", "column.blocks": "Blokusita uzeri",
"column.bookmarks": "Lektosigni", "column.bookmarks": "Lektosigni",
@ -292,6 +370,9 @@
"emoji_button.search_results": "Trovuri", "emoji_button.search_results": "Trovuri",
"emoji_button.symbols": "Simboli", "emoji_button.symbols": "Simboli",
"emoji_button.travel": "Vizito & Plasi", "emoji_button.travel": "Vizito & Plasi",
"empty_column.account_featured.me": "Vu ne estalas irga ankore. Ka vu savas?",
"empty_column.account_featured.other": "{acct} ne estalas irga ankore. Ka vu savas?",
"empty_column.account_featured_other.unknown": "Ca konto ne estalas irga ankore.",
"empty_column.account_hides_collections": "Ca uzanto selektis ne publikigar ca informo", "empty_column.account_hides_collections": "Ca uzanto selektis ne publikigar ca informo",
"empty_column.account_suspended": "Konto restriktesis", "empty_column.account_suspended": "Konto restriktesis",
"empty_column.account_timeline": "No toots here!", "empty_column.account_timeline": "No toots here!",
@ -324,6 +405,14 @@
"explore.trending_links": "Novaji", "explore.trending_links": "Novaji",
"explore.trending_statuses": "Posti", "explore.trending_statuses": "Posti",
"explore.trending_tags": "Hashtagi", "explore.trending_tags": "Hashtagi",
"featured_carousel.current": "<sr>Afisho</sr> {current, number} / {max, number}",
"featured_carousel.header": "{count, plural, one {Pinglizita Afisho} other {Pinglizita Afishi}}",
"featured_carousel.slide": "Afisho {current, number} ek {max, number}",
"featured_tags.more_items": "+{count}",
"featured_tags.suggestions": "Recente vu afishigis pri {items}. Ka adjuntez ci kom estalita gretvorti?",
"featured_tags.suggestions.add": "Adjuntez",
"featured_tags.suggestions.added": "Aranjez vua estalita gretvorti irgatempe che <link>Redaktez Profilo > Estalita gretvorti</link>.",
"featured_tags.suggestions.dismiss": "Nedanki",
"filter_modal.added.context_mismatch_explanation": "Ca filtrilgrupo ne uzesis ad informo di ca adirita afisho.", "filter_modal.added.context_mismatch_explanation": "Ca filtrilgrupo ne uzesis ad informo di ca adirita afisho.",
"filter_modal.added.context_mismatch_title": "Kontenajneparigeso!", "filter_modal.added.context_mismatch_title": "Kontenajneparigeso!",
"filter_modal.added.expired_explanation": "Ca filtrilgrupo expiris, vu bezonas chanjar expirtempo por apliko.", "filter_modal.added.expired_explanation": "Ca filtrilgrupo expiris, vu bezonas chanjar expirtempo por apliko.",
@ -388,7 +477,9 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} partoprenanto} other {{counter} partoprenanti}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} partoprenanto} other {{counter} partoprenanti}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} posto} other {{counter} posti}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} posto} other {{counter} posti}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} posto} other {{counter} posti}} hodie", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} posto} other {{counter} posti}} hodie",
"hashtag.feature": "Estalez che profilo",
"hashtag.follow": "Sequar gretvorto", "hashtag.follow": "Sequar gretvorto",
"hashtag.unfeature": "Ne estalez che profilo",
"hashtag.unfollow": "Dessequar gretvorto", "hashtag.unfollow": "Dessequar gretvorto",
"hashtags.and_other": "…e {count, plural, one {# plusa}other {# plusa}}", "hashtags.and_other": "…e {count, plural, one {# plusa}other {# plusa}}",
"hints.profiles.followers_may_be_missing": "Sequanti di ca profilo forsan ne esas hike.", "hints.profiles.followers_may_be_missing": "Sequanti di ca profilo forsan ne esas hike.",
@ -511,8 +602,10 @@
"mute_modal.you_wont_see_mentions": "Vu ne vidos posti qua mencionas lu.", "mute_modal.you_wont_see_mentions": "Vu ne vidos posti qua mencionas lu.",
"mute_modal.you_wont_see_posts": "Lu ankore povas vidar vua posti, ma vu ne vidos lua.", "mute_modal.you_wont_see_posts": "Lu ankore povas vidar vua posti, ma vu ne vidos lua.",
"navigation_bar.about": "Pri co", "navigation_bar.about": "Pri co",
"navigation_bar.account_settings": "Pasvorto e sekureso",
"navigation_bar.administration": "Administro", "navigation_bar.administration": "Administro",
"navigation_bar.advanced_interface": "Apertez per retintervizajo", "navigation_bar.advanced_interface": "Apertez per retintervizajo",
"navigation_bar.automated_deletion": "Automata postoefaco",
"navigation_bar.blocks": "Blokusita uzeri", "navigation_bar.blocks": "Blokusita uzeri",
"navigation_bar.bookmarks": "Lektosigni", "navigation_bar.bookmarks": "Lektosigni",
"navigation_bar.direct": "Privata mencioni", "navigation_bar.direct": "Privata mencioni",
@ -522,12 +615,14 @@
"navigation_bar.follow_requests": "Demandi di sequado", "navigation_bar.follow_requests": "Demandi di sequado",
"navigation_bar.followed_tags": "Hashtagi sequita", "navigation_bar.followed_tags": "Hashtagi sequita",
"navigation_bar.follows_and_followers": "Sequati e sequanti", "navigation_bar.follows_and_followers": "Sequati e sequanti",
"navigation_bar.import_export": "Importaco e exportaco",
"navigation_bar.lists": "Listi", "navigation_bar.lists": "Listi",
"navigation_bar.logout": "Ekirar", "navigation_bar.logout": "Ekirar",
"navigation_bar.moderation": "Jero", "navigation_bar.moderation": "Jero",
"navigation_bar.mutes": "Celita uzeri", "navigation_bar.mutes": "Celita uzeri",
"navigation_bar.opened_in_classic_interface": "Posti, konti e altra pagini specifika apertesas en la retovidilo klasika.", "navigation_bar.opened_in_classic_interface": "Posti, konti e altra pagini specifika apertesas en la retovidilo klasika.",
"navigation_bar.preferences": "Preferi", "navigation_bar.preferences": "Preferi",
"navigation_bar.privacy_and_reach": "Privateso e atingeso",
"navigation_bar.search": "Serchez", "navigation_bar.search": "Serchez",
"navigation_bar.search_trends": "Sercho / Tendenco", "navigation_bar.search_trends": "Sercho / Tendenco",
"not_signed_in_indicator.not_signed_in": "Vu mustas enirar por acesar ca moyeno.", "not_signed_in_indicator.not_signed_in": "Vu mustas enirar por acesar ca moyeno.",
@ -806,7 +901,7 @@
"status.admin_account": "Apertez jerintervizajo por @{name}", "status.admin_account": "Apertez jerintervizajo por @{name}",
"status.admin_domain": "Apertez jerintervizajo por {domain}", "status.admin_domain": "Apertez jerintervizajo por {domain}",
"status.admin_status": "Open this status in the moderation interface", "status.admin_status": "Open this status in the moderation interface",
"status.all_disabled": "Dishavigi e citi esas desaktivigita", "status.all_disabled": "Diskonocigi e citi esas desaktivigita",
"status.block": "Restriktez @{name}", "status.block": "Restriktez @{name}",
"status.bookmark": "Lektosigno", "status.bookmark": "Lektosigno",
"status.cancel_reblog_private": "Desrepetez", "status.cancel_reblog_private": "Desrepetez",
@ -861,10 +956,11 @@
"status.quotes_count": "{count, plural,one {{counter} cito} other {{counter} citi}}", "status.quotes_count": "{count, plural,one {{counter} cito} other {{counter} citi}}",
"status.read_more": "Lektez plu", "status.read_more": "Lektez plu",
"status.reblog": "Repetez", "status.reblog": "Repetez",
"status.reblog_or_quote": "Dishavigez o citez", "status.reblog_or_quote": "Diskonocez o citez",
"status.reblog_private": "Ankorfoye kunhavigez kun vua sequanti", "status.reblog_private": "Ankorfoye kunhavigez kun vua sequanti",
"status.reblogged_by": "{name} repetis", "status.reblogged_by": "{name} repetis",
"status.reblogs.empty": "Nulu ja repetis ca posto. Kande ulu facas lo, lu montresos hike.", "status.reblogs.empty": "Nulu ja repetis ca posto. Kande ulu facas lo, lu montresos hike.",
"status.reblogs_count": "{count, plural,one {{counter} diskonoco} other {{counter} diskonoci}}",
"status.redraft": "Efacez e riskisigez", "status.redraft": "Efacez e riskisigez",
"status.remove_bookmark": "Forigar lektosigno", "status.remove_bookmark": "Forigar lektosigno",
"status.remove_favourite": "Forigar de priziti", "status.remove_favourite": "Forigar de priziti",
@ -891,7 +987,10 @@
"subscribed_languages.save": "Sparez chanji", "subscribed_languages.save": "Sparez chanji",
"subscribed_languages.target": "Chanjez abonita lingui por {target}", "subscribed_languages.target": "Chanjez abonita lingui por {target}",
"tabs_bar.home": "Hemo", "tabs_bar.home": "Hemo",
"tabs_bar.menu": "Menuo",
"tabs_bar.notifications": "Savigi", "tabs_bar.notifications": "Savigi",
"tabs_bar.publish": "Nova afisho",
"tabs_bar.search": "Serchez",
"terms_of_service.title": "Servtermini", "terms_of_service.title": "Servtermini",
"time_remaining.days": "{number, plural, one {# dio} other {# dii}} restas", "time_remaining.days": "{number, plural, one {# dio} other {# dii}} restas",
"time_remaining.hours": "{number, plural, one {# horo} other {# hori}} restas", "time_remaining.hours": "{number, plural, one {# horo} other {# hori}} restas",
@ -929,9 +1028,12 @@
"visibility_modal.direct_quote_warning.text": "Se vu retenas la nuna preferaji, la enpozita cito esas chanjita ad ligilo.", "visibility_modal.direct_quote_warning.text": "Se vu retenas la nuna preferaji, la enpozita cito esas chanjita ad ligilo.",
"visibility_modal.direct_quote_warning.title": "Citi ne povas esar enpozita en privata mencioni", "visibility_modal.direct_quote_warning.title": "Citi ne povas esar enpozita en privata mencioni",
"visibility_modal.helper.direct_quoting": "Privata mencioni quan esas igita che Mastodon ne povas mencionita da altri.", "visibility_modal.helper.direct_quoting": "Privata mencioni quan esas igita che Mastodon ne povas mencionita da altri.",
"visibility_modal.helper.privacy_editing": "Videbleso ne povas esar chanjita pos afisho esar publikigita.",
"visibility_modal.helper.privacy_private_self_quote": "Suciti di privata afishi ne povas esar publikigita.", "visibility_modal.helper.privacy_private_self_quote": "Suciti di privata afishi ne povas esar publikigita.",
"visibility_modal.helper.private_quoting": "Nursequanta afishi quan igita che Mastodon ne povas esar citita da altri.", "visibility_modal.helper.private_quoting": "Nursequanta afishi quan igita che Mastodon ne povas esar citita da altri.",
"visibility_modal.helper.unlisted_quoting": "Kande personi citas vu, anke lia afisho esar celitos de tendenca tempolinei.", "visibility_modal.helper.unlisted_quoting": "Kande personi citas vu, anke lia afisho esar celitos de tendenca tempolinei.",
"visibility_modal.instructions": "Kontrolez qua povas interagar kun ca afisho. Vu povas irar ad <link>Preferaji > Afishigpredeterminitaji</link>.",
"visibility_modal.privacy_label": "Videbleso",
"visibility_modal.quote_followers": "Nur sequanti", "visibility_modal.quote_followers": "Nur sequanti",
"visibility_modal.quote_label": "Qua povas citar", "visibility_modal.quote_label": "Qua povas citar",
"visibility_modal.quote_nobody": "Nur me", "visibility_modal.quote_nobody": "Nur me",

View File

@ -125,6 +125,7 @@
"annual_report.summary.followers.new_followers": "{count, plural, other {新しいフォロワー}}", "annual_report.summary.followers.new_followers": "{count, plural, other {新しいフォロワー}}",
"annual_report.summary.highlighted_post.boost_count": "この投稿は {count, plural, other {# 回}}ブーストされました。", "annual_report.summary.highlighted_post.boost_count": "この投稿は {count, plural, other {# 回}}ブーストされました。",
"annual_report.summary.highlighted_post.favourite_count": "この投稿は {count, plural, other {# 回}}お気に入りされました。", "annual_report.summary.highlighted_post.favourite_count": "この投稿は {count, plural, other {# 回}}お気に入りされました。",
"annual_report.summary.highlighted_post.reply_count": "この投稿には {count, plural, other {# 回}}返信がありました。",
"annual_report.summary.highlighted_post.title": "最も人気のある投稿", "annual_report.summary.highlighted_post.title": "最も人気のある投稿",
"annual_report.summary.most_used_app.most_used_app": "最も使用されているアプリ", "annual_report.summary.most_used_app.most_used_app": "最も使用されているアプリ",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "最も使用されたハッシュタグ", "annual_report.summary.most_used_hashtag.most_used_hashtag": "最も使用されたハッシュタグ",

View File

@ -1,6 +1,7 @@
{ {
"about.blocks": "Модерациялана торган серверлар", "about.blocks": "Модерациялана торган серверлар",
"about.contact": "Бәйләнеш:", "about.contact": "Бәйләнеш:",
"about.default_locale": "Килешү буенча",
"about.disclaimer": "Mastodon-бушлай ачык чыганак программасы һәм Mastodon gmbh сәүдә маркасы.", "about.disclaimer": "Mastodon-бушлай ачык чыганак программасы һәм Mastodon gmbh сәүдә маркасы.",
"about.domain_blocks.no_reason_available": "Сәбәбе юк", "about.domain_blocks.no_reason_available": "Сәбәбе юк",
"about.domain_blocks.preamble": "Mastodon гадәттә сезгә бүтән fediverse серверыннан эчтәлекне карарга һәм аның белән кулланучылар белән аралашырга мөмкинлек бирә. Бу конкрет серверда ясалган искәрмәләр.", "about.domain_blocks.preamble": "Mastodon гадәттә сезгә бүтән fediverse серверыннан эчтәлекне карарга һәм аның белән кулланучылар белән аралашырга мөмкинлек бирә. Бу конкрет серверда ясалган искәрмәләр.",
@ -12,17 +13,28 @@
"about.not_available": "Бу серверда бу мәгълүмат юк иде.", "about.not_available": "Бу серверда бу мәгълүмат юк иде.",
"about.powered_by": "{mastodon} нигезендә үзәкчелеге бетерелгән социаль челтәр нигезендә", "about.powered_by": "{mastodon} нигезендә үзәкчелеге бетерелгән социаль челтәр нигезендә",
"about.rules": "Сервер кагыйдәләре", "about.rules": "Сервер кагыйдәләре",
"account.account_note_header": "Шәхси искәрмәләр",
"account.activity": "Активлык", "account.activity": "Активлык",
"account.add_note": "Шәхси искәрмә өстәү",
"account.add_or_remove_from_list": "Исемлеккә кушу яки бетерү", "account.add_or_remove_from_list": "Исемлеккә кушу яки бетерү",
"account.badges.admin": "Админ",
"account.badges.blocked": "Блокланган",
"account.badges.bot": "Бот", "account.badges.bot": "Бот",
"account.badges.domain_blocked": "Блокланган доменнар",
"account.badges.group": "Төркем", "account.badges.group": "Төркем",
"account.badges.muted": "Тавышсыз",
"account.badges.muted_until": "{until}-га кадәр игнорлау",
"account.block": "@{name} кулланучыны блоклау", "account.block": "@{name} кулланучыны блоклау",
"account.block_domain": "{domain} доменын блоклау", "account.block_domain": "{domain} доменын блоклау",
"account.block_short": "Блокла", "account.block_short": "Блокла",
"account.blocked": "Блокланган", "account.blocked": "Блокланган",
"account.blocking": "Блокланган",
"account.cancel_follow_request": "Киләсе сорау", "account.cancel_follow_request": "Киләсе сорау",
"account.copy": "Профиль сылтамасын күчереп ал", "account.copy": "Профиль сылтамасын күчереп ал",
"account.direct": "{name}-ны шәхсән икә алу",
"account.disable_notifications": "@{name} язулары өчен белдерүләр сүндерү", "account.disable_notifications": "@{name} язулары өчен белдерүләр сүндерү",
"account.domain_blocking": "Блокланган домен",
"account.edit_note": "Шәхси искәрмәне төзәтү",
"account.edit_profile": "Профильне үзгәртү", "account.edit_profile": "Профильне үзгәртү",
"account.edit_profile_short": "Үзгәрт", "account.edit_profile_short": "Үзгәрт",
"account.enable_notifications": "@{name} язулары өчен белдерүләр яндыру", "account.enable_notifications": "@{name} язулары өчен белдерүләр яндыру",

View File

@ -1071,7 +1071,7 @@
"remove_quote_hint.button_label": "Đã hiểu", "remove_quote_hint.button_label": "Đã hiểu",
"remove_quote_hint.message": "Bạn cũng có thể làm trong menu tùy chọn {icon}", "remove_quote_hint.message": "Bạn cũng có thể làm trong menu tùy chọn {icon}",
"remove_quote_hint.title": "Gỡ tút mà bạn đã trích dẫn?", "remove_quote_hint.title": "Gỡ tút mà bạn đã trích dẫn?",
"reply_indicator.attachments": "{count, plural, other {# tập tin đính kèm}}", "reply_indicator.attachments": "{count, plural, other {# đính kèm}}",
"reply_indicator.cancel": "Hủy bỏ", "reply_indicator.cancel": "Hủy bỏ",
"reply_indicator.poll": "Vốt", "reply_indicator.poll": "Vốt",
"report.block": "Chặn", "report.block": "Chặn",
@ -1283,14 +1283,14 @@
"units.short.thousand": "{count}K", "units.short.thousand": "{count}K",
"upload_area.title": "Kéo và thả để tải lên", "upload_area.title": "Kéo và thả để tải lên",
"upload_button.label": "Thêm media (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_button.label": "Thêm media (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Tập tin tải lên vượt quá giới hạn cho phép.", "upload_error.limit": "Tệp tải lên vượt quá giới hạn cho phép.",
"upload_error.poll": "Không cho phép đính kèm tập tin.", "upload_error.poll": "Không cho phép đính kèm tệp.",
"upload_error.quote": "Không cho phép đính kèm tập tin với trích dẫn.", "upload_error.quote": "Không cho phép đính kèm tệp với trích dẫn.",
"upload_form.drag_and_drop.instructions": "Để chọn tập tin đính kèm, hãy nhấn phím cách hoặc phím Enter. Trong khi kéo, sử dụng các phím mũi tên để di chuyển tập tin đính kèm theo bất kỳ hướng nào. Nhấn phím cách hoặc phím Enter một lần nữa để thả tập tin đính kèm vào vị trí mới hoặc nhấn phím thoát để hủy.", "upload_form.drag_and_drop.instructions": "Để chọn tệp đính kèm, hãy nhấn phím cách hoặc phím Enter. Trong khi kéo, sử dụng các phím mũi tên để di chuyển tệp đính kèm theo bất kỳ hướng nào. Nhấn phím cách hoặc phím Enter một lần nữa để thả tệp đính kèm vào vị trí mới hoặc nhấn phím thoát để hủy.",
"upload_form.drag_and_drop.on_drag_cancel": "Kéo thả đã bị hủy bỏ. Tập tin đính kèm {item} bị bỏ qua.", "upload_form.drag_and_drop.on_drag_cancel": "Kéo thả đã bị hủy bỏ. Tệp đính kèm {item} bị bỏ qua.",
"upload_form.drag_and_drop.on_drag_end": "Tập tin đính kèm {item} bị bỏ qua.", "upload_form.drag_and_drop.on_drag_end": "Tệp đính kèm {item} bị bỏ qua.",
"upload_form.drag_and_drop.on_drag_over": "Tập tin đính kèm {item} đã bị dời.", "upload_form.drag_and_drop.on_drag_over": "Tệp đính kèm {item} bị di chuyển.",
"upload_form.drag_and_drop.on_drag_start": "Đã chọn tập tin đính kèm {item}.", "upload_form.drag_and_drop.on_drag_start": "Đã chọn tệp đính kèm {item}.",
"upload_form.edit": "Biên tập", "upload_form.edit": "Biên tập",
"upload_progress.label": "Đang tải lên...", "upload_progress.label": "Đang tải lên...",
"upload_progress.processing": "Đang tải lên…", "upload_progress.processing": "Đang tải lên…",

View File

@ -101,6 +101,7 @@ export const accountDefaultValues: AccountShape = {
limited: false, limited: false,
moved: null, moved: null,
hide_collections: false, hide_collections: false,
email_subscriptions: false,
// This comes from `ApiMutedAccountJSON`, but we should eventually // This comes from `ApiMutedAccountJSON`, but we should eventually
// store that in a different object. // store that in a different object.
mute_expires_at: null, mute_expires_at: null,

View File

@ -12,7 +12,7 @@ export function isProduction() {
else return import.meta.env.PROD; else return import.meta.env.PROD;
} }
export type ServerFeatures = 'fasp' | 'collections' | 'profile_redesign'; export type ServerFeatures = 'fasp' | 'collections';
export function isServerFeatureEnabled(feature: ServerFeatures) { export function isServerFeatureEnabled(feature: ServerFeatures) {
return initialState?.features.includes(feature) ?? false; return initialState?.features.includes(feature) ?? false;

View File

@ -5,7 +5,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
return accept_follow_for_relay if relay_follow? return accept_follow_for_relay if relay_follow?
return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil?
return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil? return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil?
return accept_feature_request! if Mastodon::Feature.collections_federation_enabled? && feature_request_from_object.present? return accept_feature_request! if Mastodon::Feature.collections_enabled? && feature_request_from_object.present?
case @object['type'] case @object['type']
when 'Follow' when 'Follow'

View File

@ -13,12 +13,12 @@ class ActivityPub::Activity::Add < ActivityPub::Activity
add_featured add_featured
end end
when @account.collections_url when @account.collections_url
return unless Mastodon::Feature.collections_federation_enabled? return unless Mastodon::Feature.collections_enabled?
add_collection add_collection
else else
@collection = @account.collections.find_by(uri: value_or_id(@json['target'])) @collection = @account.collections.find_by(uri: value_or_id(@json['target']))
add_collection_item if @collection && Mastodon::Feature.collections_federation_enabled? add_collection_item if @collection && Mastodon::Feature.collections_enabled?
end end
end end

View File

@ -3,7 +3,7 @@
class ActivityPub::Activity::Delete < ActivityPub::Activity class ActivityPub::Activity::Delete < ActivityPub::Activity
def perform def perform
return delete_person if @account.uri == object_uri return delete_person if @account.uri == object_uri
return delete_feature_authorization! unless !Mastodon::Feature.collections_federation_enabled? || feature_authorization_from_object.nil? return delete_feature_authorization! unless !Mastodon::Feature.collections_enabled? || feature_authorization_from_object.nil?
delete_object delete_object
end end

View File

@ -4,7 +4,7 @@ class ActivityPub::Activity::FeatureRequest < ActivityPub::Activity
include Payloadable include Payloadable
def perform def perform
return unless Mastodon::Feature.collections_federation_enabled? return unless Mastodon::Feature.collections_enabled?
return if non_matching_uri_hosts?(@account.uri, @json['id']) return if non_matching_uri_hosts?(@account.uri, @json['id'])
@collection = find_or_fetch_collection @collection = find_or_fetch_collection

View File

@ -13,7 +13,7 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
update_account update_account
elsif supported_object_type? || converted_object_type? elsif supported_object_type? || converted_object_type?
update_status update_status
elsif equals_or_includes_any?(@object['type'], ['FeaturedCollection']) && Mastodon::Feature.collections_federation_enabled? elsif equals_or_includes_any?(@object['type'], ['FeaturedCollection']) && Mastodon::Feature.collections_enabled?
update_collection update_collection
end end
end end

View File

@ -30,7 +30,7 @@ class EmailSubscriptionMailer < ApplicationMailer
@statuses = statuses @statuses = statuses
I18n.with_locale(locale) do I18n.with_locale(locale) do
mail subject: default_i18n_subject(count: @statuses.size, name: @subscription.account.display_name, excerpt: @statuses.first.text.truncate(17)) mail subject: I18n.t(@statuses.size == 1 ? 'singular' : 'plural', scope: 'email_subscription_mailer.notification.subject', name: @subscription.account.display_name, excerpt: @statuses.first.text.truncate(17))
end end
end end

View File

@ -470,7 +470,6 @@ class Account < ApplicationRecord
def featureable_by?(other_account) def featureable_by?(other_account)
return discoverable? if local? return discoverable? if local?
return false unless Mastodon::Feature.collections_federation_enabled?
feature_policy_for_account(other_account).in?(%i(automatic manual)) feature_policy_for_account(other_account).in?(%i(automatic manual))
end end

View File

@ -20,6 +20,7 @@ class EmailSubscription < ApplicationRecord
normalizes :email, with: ->(str) { str.squish.downcase } normalizes :email, with: ->(str) { str.squish.downcase }
validates :email, presence: true, email_address: true, uniqueness: { scope: :account_id } validates :email, presence: true, email_address: true, uniqueness: { scope: :account_id }
validates :email, email_mx: true, if: -> { email_changed? && !Rails.env.local? }
scope :confirmed, -> { where.not(confirmed_at: nil) } scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :unconfirmed, -> { where(confirmed_at: nil) } scope :unconfirmed, -> { where(confirmed_at: nil) }

View File

@ -93,9 +93,9 @@ class User < ApplicationRecord
validates :invite_request, presence: true, on: :create, if: :invite_text_required? validates :invite_request, presence: true, on: :create, if: :invite_text_required?
validates :email, presence: true, email_address: true validates :email, presence: true, email_address: true
validates :email, email_mx: { attempt_ip: :sign_up_ip }, if: :validate_email_dns?
validates_with UserEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? } validates_with UserEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
validates_with EmailMxValidator, if: :validate_email_dns?
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
# Honeypot/anti-spam fields # Honeypot/anti-spam fields

View File

@ -9,12 +9,14 @@ class ActivityPub::FetchRemoteKeyService < BaseService
def call(uri, suppress_errors: true) def call(uri, suppress_errors: true)
raise Error, 'No key URI given' if uri.blank? raise Error, 'No key URI given' if uri.blank?
@suppress_errors = suppress_errors
@uri = uri
@json = fetch_resource(uri, false) @json = fetch_resource(uri, false)
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil? raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json) || (supported_security_context?(@json) && @json['owner'].present? && !actor_type?) raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json) || (supported_security_context?(@json) && @json['owner'].present? && !actor_type?)
raise Error, "Unexpected object type for key #{uri}" unless expected_type? raise Error, "Unexpected object type for key #{uri}" unless expected_type?
return Keypair.from_legacy_account(find_actor(@json['id'], @json, suppress_errors), uri: uri) if actor_type? return keypair_from_actor_json(@json['id'], @json) if actor_type?
@owner = fetch_resource(owner_uri, true) @owner = fetch_resource(owner_uri, true)
@ -23,8 +25,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
raise Error, "Unexpected object type for actor #{owner_uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_owner_type? raise Error, "Unexpected object type for actor #{owner_uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_owner_type?
raise Error, "publicKey id for #{owner_uri} does not correspond to #{@json['id']}" unless confirmed_owner? raise Error, "publicKey id for #{owner_uri} does not correspond to #{@json['id']}" unless confirmed_owner?
# TODO: change to fetch and persist key keypair_from_actor_json(owner_uri, @owner)
Keypair.from_legacy_account(find_actor(owner_uri, @owner, suppress_errors), uri: uri)
rescue Error => e rescue Error => e
Rails.logger.debug { "Fetching key #{uri} failed: #{e.message}" } Rails.logger.debug { "Fetching key #{uri} failed: #{e.message}" }
raise unless suppress_errors raise unless suppress_errors
@ -32,9 +33,19 @@ class ActivityPub::FetchRemoteKeyService < BaseService
private private
def find_actor(uri, prefetched_body, suppress_errors) def keypair_from_actor_json(actor_uri, actor_json)
actor = find_actor(actor_uri, actor_json)
return if actor.nil?
keypair = actor.keypairs.find_by(uri: @uri)
return keypair if keypair.present?
Keypair.from_legacy_account(actor, uri: @uri) if actor.public_key.present?
end
def find_actor(uri, prefetched_body)
actor = ActivityPub::TagManager.instance.uri_to_actor(uri) actor = ActivityPub::TagManager.instance.uri_to_actor(uri)
actor ||= ActivityPub::FetchRemoteActorService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors) actor ||= ActivityPub::FetchRemoteActorService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: @suppress_errors)
actor actor
end end

View File

@ -65,7 +65,7 @@ class ActivityPub::ProcessAccountService < BaseService
unless @options[:only_key] || @account.suspended? unless @options[:only_key] || @account.suspended?
check_featured_collection! if @json['featured'].present? check_featured_collection! if @json['featured'].present?
check_featured_tags_collection! if @json['featuredTags'].present? check_featured_tags_collection! if @json['featuredTags'].present?
check_featured_collections_collection! if @json['featuredCollections'].present? && Mastodon::Feature.collections_federation_enabled? check_featured_collections_collection! if @json['featuredCollections'].present? && Mastodon::Feature.collections_enabled?
check_links! if @account.fields.any?(&:requires_verification?) check_links! if @account.fields.any?(&:requires_verification?)
end end

View File

@ -11,10 +11,8 @@ class AddAccountToCollectionService
@collection_item = create_collection_item @collection_item = create_collection_item
if Mastodon::Feature.collections_federation_enabled? distribute_add_activity if @account.local?
distribute_add_activity if @account.local? distribute_feature_request_activity if @account.remote?
distribute_feature_request_activity if @account.remote?
end
@collection_item @collection_item
end end

View File

@ -9,10 +9,8 @@ class CreateCollectionService
@collection.save! @collection.save!
if Mastodon::Feature.collections_federation_enabled? distribute_add_activity
distribute_add_activity distribute_feature_request_activities
distribute_feature_request_activities
end
@collection @collection
end end

View File

@ -7,7 +7,7 @@ class DeleteCollectionItemService
if collection_item.local? if collection_item.local?
revoke ? @collection_item.revoke! : @collection_item.destroy! revoke ? @collection_item.revoke! : @collection_item.destroy!
distribute_remove_activity if Mastodon::Feature.collections_federation_enabled? distribute_remove_activity
else else
collection_item.destroy! collection_item.destroy!
end end

View File

@ -5,7 +5,7 @@ class DeleteCollectionService
@collection = collection @collection = collection
@collection.destroy! @collection.destroy!
distribute_remove_activity if Mastodon::Feature.collections_federation_enabled? distribute_remove_activity
end end
private private

View File

@ -10,7 +10,7 @@ class RevokeCollectionItemService < BaseService
@collection_item.revoke! @collection_item.revoke!
distribute_stamp_deletion! if Mastodon::Feature.collections_federation_enabled? && @collection_item.remote? distribute_stamp_deletion! if @collection_item.remote?
end end
private private

View File

@ -7,7 +7,7 @@ class UpdateCollectionService
@collection = collection @collection = collection
@collection.update!(params) @collection.update!(params)
distribute_update_activity if Mastodon::Feature.collections_federation_enabled? distribute_update_activity
end end
private private

View File

@ -2,21 +2,21 @@
require 'resolv' require 'resolv'
class EmailMxValidator < ActiveModel::Validator class EmailMxValidator < ActiveModel::EachValidator
def validate(user) def validate_each(record, attribute, value)
return if user.email.blank? return if value.blank?
domain = get_domain(user.email) domain = get_domain(value)
if domain.blank? || domain.include?('..') if domain.blank? || domain.include?('..')
user.errors.add(:email, :invalid) record.errors.add(attribute, :invalid)
elsif !on_allowlist?(domain) elsif !on_allowlist?(domain)
resolved_ips, resolved_domains = resolve_mx(domain) resolved_ips, resolved_domains = resolve_mx(domain)
if resolved_ips.empty? if resolved_ips.empty?
user.errors.add(:email, :unreachable) record.errors.add(attribute, :unreachable)
elsif email_domain_blocked?(resolved_domains, user.sign_up_ip) elsif email_domain_blocked?([domain, *resolved_domains], options[:attempt_ip].is_a?(Symbol) ? record.public_send(options[:attempt_ip]) : nil)
user.errors.add(:email, :blocked) record.errors.add(attribute, :blocked)
end end
end end
end end

View File

@ -1,4 +1,4 @@
<%= t '.title', count: @statuses.size, name: display_name(@subscription.account), excerpt: truncate(@statuses.first.text, length: 17) %> <%= t @statuses.size == 1 ? 'singular' : 'plural', scope: 'email_subscription_mailer.notification.title', name: display_name(@subscription.account), excerpt: truncate(@statuses.first.text, length: 17) %>
=== ===

View File

@ -32,6 +32,12 @@ ja:
attributes: attributes:
url: url:
invalid: は無効なURLです invalid: は無効なURLです
collection:
attributes:
collection_items:
too_many: は多すぎます。 %{count} 以下にしてください
tag:
unusable: は使用できません
doorkeeper/application: doorkeeper/application:
attributes: attributes:
website: website:

View File

@ -2242,7 +2242,7 @@ be:
details: 'Вось падрабязнасці ўваходу:' details: 'Вось падрабязнасці ўваходу:'
explanation: Мы заўважылі ўваход у ваш уліковы запіс з новага IP-адрасу. explanation: Мы заўважылі ўваход у ваш уліковы запіс з новага IP-адрасу.
further_actions_html: Калі гэта былі не вы, раім вам неадкладна %{action}, а таксама ўключыць двухфактарную аўтэнтыфікацыю, каб захаваць бяспеку вашага ўліковага запісу. further_actions_html: Калі гэта былі не вы, раім вам неадкладна %{action}, а таксама ўключыць двухфактарную аўтэнтыфікацыю, каб захаваць бяспеку вашага ўліковага запісу.
subject: У вас уліковы запіс зайшлі з новага IP-адрасу subject: У Ваш уліковы запіс зайшлі з новага IP-адрасу
title: Новы ўваход title: Новы ўваход
terms_of_service_changed: terms_of_service_changed:
agreement: Працягваючы карыстацца %{domain}, Вы пагаджаецеся з гэтымі ўмовамі. Калі Вы не згодныя з абноўленымі ўмовамі, то можаце ў любы момант адмовіцца ад пагаднення з %{domain}, выдаліўшы свой профіль. agreement: Працягваючы карыстацца %{domain}, Вы пагаджаецеся з гэтымі ўмовамі. Калі Вы не згодныя з абноўленымі ўмовамі, то можаце ў любы момант адмовіцца ад пагаднення з %{domain}, выдаліўшы свой профіль.

View File

@ -7,6 +7,7 @@ ja:
send_paranoid_instructions: もしあなたのメールアドレスが登録されていれば、まもなくメールアドレスの確認の方法が記載されたメールが送信されます。 send_paranoid_instructions: もしあなたのメールアドレスが登録されていれば、まもなくメールアドレスの確認の方法が記載されたメールが送信されます。
failure: failure:
already_authenticated: 既にログイン済みです。 already_authenticated: 既にログイン済みです。
closed_registrations: アカウントの登録はネットワークポリシーによりブロックされました。これが誤りだと思われる場合は %{email} まで連絡してください。
inactive: あなたのアカウントはまだ有効化されていません。 inactive: あなたのアカウントはまだ有効化されていません。
invalid: "%{authentication_keys}かパスワードが誤っています。" invalid: "%{authentication_keys}かパスワードが誤っています。"
last_attempt: あと1回失敗するとアカウントがロックされます。 last_attempt: あと1回失敗するとアカウントがロックされます。

View File

@ -83,6 +83,10 @@ gd:
access_denied: Dhiùlt sealbhadair a ghoireis no am frithealaiche ùghdarrachaidh an t-iarrtas. access_denied: Dhiùlt sealbhadair a ghoireis no am frithealaiche ùghdarrachaidh an t-iarrtas.
credential_flow_not_configured: Dhfhàillig le sruth cruthachadh teisteas facail-fhaire do shealbhadair a ghoireis ri linn Doorkeeper.configure.resource_owner_from_credentials gun rèiteachadh. credential_flow_not_configured: Dhfhàillig le sruth cruthachadh teisteas facail-fhaire do shealbhadair a ghoireis ri linn Doorkeeper.configure.resource_owner_from_credentials gun rèiteachadh.
invalid_client: Dhfhàillig le dearbhadh a chliant ri linn cliant nach aithne dhuinn, dearbhadh cliant nach deach gabhail a-staigh no dòigh dearbhaidh ris nach cuirear taic. invalid_client: Dhfhàillig le dearbhadh a chliant ri linn cliant nach aithne dhuinn, dearbhadh cliant nach deach gabhail a-staigh no dòigh dearbhaidh ris nach cuirear taic.
invalid_code_challenge_method:
one: Feumaidh an code_challenge_method a bhith na %{challenge_methods}.
other: Feumaidh an code_challenge_method a bhith aon de %{challenge_methods}.
zero: Cha chuir frithealaiche an ùghdarrachaidh taic ri PKCE air sgàth s nach eil luach code_challenge_method ann ris an gabhar.
invalid_grant: Chan eil an t-ùghdarrachadh a chaidh a thoirt seachad dligheach, dhfhalbh an ùine air, chaidh a chùl-ghairm no chan eil e a-rèir URI an ath-stiùiridh a chaidh a chleachdadh san iarrtas ùghdarrachaidh no chaidh fhoillseachadh le cliant eile. invalid_grant: Chan eil an t-ùghdarrachadh a chaidh a thoirt seachad dligheach, dhfhalbh an ùine air, chaidh a chùl-ghairm no chan eil e a-rèir URI an ath-stiùiridh a chaidh a chleachdadh san iarrtas ùghdarrachaidh no chaidh fhoillseachadh le cliant eile.
invalid_redirect_uri: Chan eil an URI ath-stiùiridh a chaidh a ghabhail a-staigh dligheach. invalid_redirect_uri: Chan eil an URI ath-stiùiridh a chaidh a ghabhail a-staigh dligheach.
invalid_request: invalid_request:

View File

@ -83,6 +83,10 @@ ja:
access_denied: リソースの所有者または認証サーバーが要求を拒否しました。 access_denied: リソースの所有者または認証サーバーが要求を拒否しました。
credential_flow_not_configured: リソース所有者のパスワード Doorkeeper.configure.resource_owner_from_credentials が設定されていないためクレデンシャルフローに失敗しました。 credential_flow_not_configured: リソース所有者のパスワード Doorkeeper.configure.resource_owner_from_credentials が設定されていないためクレデンシャルフローに失敗しました。
invalid_client: 不明なクライアントであるか、クライアント情報が含まれていない、またはサポートされていない認証方法のため、クライアントの認証に失敗しました。 invalid_client: 不明なクライアントであるか、クライアント情報が含まれていない、またはサポートされていない認証方法のため、クライアントの認証に失敗しました。
invalid_code_challenge_method:
one: code_challenge_methodは %{challenge_methods} でなければなりません。
other: code_challenge_methodは %{challenge_methods} のいずれかでなければなりません。
zero: 認証サーバーが受け入れる code_challenge_method 値がないため、PKCE をサポートしません。
invalid_grant: 指定された認証許可は無効であるか、期限切れ、取り消されている、リダイレクトURIの不一致、または別のクライアントに発行されています。 invalid_grant: 指定された認証許可は無効であるか、期限切れ、取り消されている、リダイレクトURIの不一致、または別のクライアントに発行されています。
invalid_redirect_uri: 無効なリダイレクトURIが含まれています。 invalid_redirect_uri: 無効なリダイレクトURIが含まれています。
invalid_request: invalid_request:

View File

@ -136,7 +136,7 @@ vi:
follow: Theo dõi, Phớt lờ và Chặn follow: Theo dõi, Phớt lờ và Chặn
follows: Đang theo dõi follows: Đang theo dõi
lists: Danh sách lists: Danh sách
media: Tập tin đính kèm media: Tệp đính kèm
mutes: Đã phớt lờ mutes: Đã phớt lờ
notifications: Thông báo notifications: Thông báo
profile: Hồ sơ Mastodon của bạn profile: Hồ sơ Mastodon của bạn
@ -194,7 +194,7 @@ vi:
write:filters: tạo bộ lọc write:filters: tạo bộ lọc
write:follows: theo dõi ai đó write:follows: theo dõi ai đó
write:lists: tạo danh sách write:lists: tạo danh sách
write:media: tải lên tập tin write:media: tải lên tệp
write:mutes: phớt lờ tài khoản và thảo luận write:mutes: phớt lờ tài khoản và thảo luận
write:notifications: xóa thông báo write:notifications: xóa thông báo
write:reports: báo cáo write:reports: báo cáo

View File

@ -1218,7 +1218,7 @@ el:
created_msg: Δημιουργήθηκε νέο ψευδώνυμο. Τώρα μπορείς να ξεκινήσεις τη μεταφορά από τον παλιό λογαριασμό. created_msg: Δημιουργήθηκε νέο ψευδώνυμο. Τώρα μπορείς να ξεκινήσεις τη μεταφορά από τον παλιό λογαριασμό.
deleted_msg: Αφαιρέθηκε το ψευδώνυμο. Η μεταφορά από εκείνον τον λογαριασμό σε αυτόν εδώ δε θα είναι πλέον δυνατή. deleted_msg: Αφαιρέθηκε το ψευδώνυμο. Η μεταφορά από εκείνον τον λογαριασμό σε αυτόν εδώ δε θα είναι πλέον δυνατή.
empty: Δεν έχεις ψευδώνυμα. empty: Δεν έχεις ψευδώνυμα.
hint_html: Αν θέλεις να μετακινηθείς από έναν άλλο λογαριασμό σε αυτόν εδώ, εδώ μπορείς να δημιουργήσεις ένα ψευδώνυμο, πράγμα που απαιτείται πριν προχωρήσεις για να μεταφέρεις τους ακολούθους σου από τον παλιό λογαριασμό σε αυτόν εδώ. Η ενέργεια αυτή είναι <strong>ακίνδυνη και αναστρέψιμη</strong>.<strong>Η μετακόμιση του λογαριασμού ξεκινάει από τον παλιό λογαριασμό</strong>. hint_html: Αν θέλεις να μετακινηθείς από έναν άλλο λογαριασμό σε αυτόν εδώ, εδώ μπορείς να δημιουργήσεις ένα ψευδώνυμο, το οποίο απαιτείται πριν προχωρήσεις για να μεταφέρεις τους ακολούθους σου από τον παλιό λογαριασμό σε αυτόν εδώ. Η ενέργεια αυτή είναι <strong>ακίνδυνη και αναστρέψιμη</strong>. <strong>Η μετακίνηση του λογαριασμού ξεκινάει από τον παλιό λογαριασμό</strong>.
remove: Αποσύνδεση ψευδώνυμου remove: Αποσύνδεση ψευδώνυμου
appearance: appearance:
advanced_settings: Προχωρημένες ρυθμίσεις advanced_settings: Προχωρημένες ρυθμίσεις
@ -1263,7 +1263,7 @@ el:
welcome_title: Καλώς ήρθες, %{name}! welcome_title: Καλώς ήρθες, %{name}!
wrong_email_hint: Εάν αυτή η διεύθυνση email δεν είναι σωστή, μπορείς να την αλλάξεις στις ρυθμίσεις λογαριασμού. wrong_email_hint: Εάν αυτή η διεύθυνση email δεν είναι σωστή, μπορείς να την αλλάξεις στις ρυθμίσεις λογαριασμού.
delete_account: Διαγραφή λογαριασμού delete_account: Διαγραφή λογαριασμού
delete_account_html: Αν θέλεις να διαγράψεις το λογαριασμό σου, μπορείς <a href="%{path}">να συνεχίσεις εδώ</a>. Θα σου ζητηθεί επιβεβαίωση. delete_account_html: Αν θέλεις να διαγράψεις το λογαριασμό σου, μπορείς <a href="%{path}">να προχωρήσεις εδώ</a>. Θα σου ζητηθεί επιβεβαίωση.
description: description:
prefix_invited_by_user: Ο/Η @%{name} σε προσκαλεί να γίνεις μέλος αυτού του διακομιστή του Mastodon! prefix_invited_by_user: Ο/Η @%{name} σε προσκαλεί να γίνεις μέλος αυτού του διακομιστή του Mastodon!
prefix_sign_up: Κάνε εγγραφή στο Mastodon σήμερα! prefix_sign_up: Κάνε εγγραφή στο Mastodon σήμερα!
@ -1278,7 +1278,7 @@ el:
login: Σύνδεση login: Σύνδεση
logout: Αποσύνδεση logout: Αποσύνδεση
migrate_account: Μεταφορά σε διαφορετικό λογαριασμό migrate_account: Μεταφορά σε διαφορετικό λογαριασμό
migrate_account_html: Αν θέλεις να ανακατευθύνεις αυτό τον λογαριασμό σε έναν διαφορετικό, μπορείς να το <a href="%{path}">διαμορφώσεις εδώ</a>. migrate_account_html: Αν θέλεις να ανακατευθύνεις αυτόν το λογαριασμό σε έναν διαφορετικό, μπορείς να το <a href="%{path}">διαμορφώσεις εδώ</a>.
or_log_in_with: Ή συνδέσου με or_log_in_with: Ή συνδέσου με
progress: progress:
confirm: Επιβεβαίωση email confirm: Επιβεβαίωση email
@ -1372,18 +1372,18 @@ el:
x_seconds: "%{count}δ" x_seconds: "%{count}δ"
deletes: deletes:
challenge_not_passed: Οι πληροφορίες που εισήγαγες δεν ήταν σωστές challenge_not_passed: Οι πληροφορίες που εισήγαγες δεν ήταν σωστές
confirm_password: Γράψε το τρέχον συνθηματικό σου για να πιστοποιήσεις την ταυτότητά σου confirm_password: Γράψε το τρέχον συνθηματικό σου για να επαληθεύσεις την ταυτότητά σου
confirm_username: Γράψε το όνομα χρήστη σου για επιβεβαίωση confirm_username: Γράψε το όνομα χρήστη σου για επιβεβαίωση
proceed: Διαγραφή λογαριασμού proceed: Διαγραφή λογαριασμού
success_msg: Ο λογαριασμός σου διαγράφηκε με επιτυχία success_msg: Ο λογαριασμός σου διαγράφηκε με επιτυχία
warning: warning:
before: 'Πριν συνεχίσεις, παρακαλούμε να διαβάσεις τις παρακάτω σημειώσεις προσεκτικά:' before: 'Πριν συνεχίσεις, παρακαλούμε διάβασε αυτές τις σημειώσεις προσεκτικά:'
caches: Περιεχόμενο που έχει αποθηκευτεί προσωρινά σε άλλους διακομιστές ίσως παραμείνει caches: Περιεχόμενο που έχει αποθηκευτεί προσωρινά σε άλλους διακομιστές ίσως παραμείνει
data_removal: Οι αναρτήσεις σου και άλλα δεδομένα θα διαγραφούν οριστικά data_removal: Οι αναρτήσεις σου και άλλα δεδομένα θα διαγραφούν οριστικά
email_change_html: Μπορείς να <a href="%{path}">αλλάξεις τη διεύθυνση email σου</a> χωρίς να διαγράψεις το λογαριασμό σου email_change_html: Μπορείς να <a href="%{path}">αλλάξεις τη διεύθυνση email σου</a> χωρίς να διαγράψεις το λογαριασμό σου
email_contact_html: Αν και πάλι δεν εμφανιστεί, μπορείς να στείλεις email στο <a href="mailto:%{email}">%{email}</a> για βοήθεια email_contact_html: Αν και πάλι δεν φτάσει, μπορείς να στείλεις email στο <a href="mailto:%{email}">%{email}</a> για βοήθεια
email_reconfirmation_html: Αν δεν έχεις λάβει το email επιβεβαίωσης, μπορείς να το <a href="%{path}">ζητήσεις ξανά</a> email_reconfirmation_html: Αν δεν λαμβάνεις το email επιβεβαίωσης, μπορείς να το <a href="%{path}">ζητήσεις ξανά</a>
irreversible: Δεν θα μπορείς να ανακτήσεις ή ενεργοποιήσεις ξανά το λογαριασμό σου irreversible: Δεν θα μπορείς να επαναφέρεις ή ενεργοποιήσεις ξανά το λογαριασμό σου
more_details_html: Για περισσότερες πληροφορίες, δες την <a href="%{terms_path}">πολιτική απορρήτου</a>. more_details_html: Για περισσότερες πληροφορίες, δες την <a href="%{terms_path}">πολιτική απορρήτου</a>.
username_available: Το όνομα χρήστη σου θα γίνει ξανά διαθέσιμο username_available: Το όνομα χρήστη σου θα γίνει ξανά διαθέσιμο
username_unavailable: Το όνομα χρήστη σου θα παραμείνει μη διαθέσιμο username_unavailable: Το όνομα χρήστη σου θα παραμείνει μη διαθέσιμο
@ -1482,7 +1482,7 @@ el:
archive_takeout: archive_takeout:
date: Ημερομηνία date: Ημερομηνία
download: Κατέβασε το αρχείο σου download: Κατέβασε το αρχείο σου
hint_html: Μπορείς να αιτηθείς ένα αρχείο των <strong>αναρτήσεων και των ανεβασμένων πολυμέσων</strong> σου. Τα δεδομένα θα είναι σε μορφή ActivityPub, προσπελάσιμα από οποιοδήποτε συμβατό πρόγραμμα. Μπορείς να αιτηθείς ένα αρχείο κάθε 7 μέρες. hint_html: Μπορείς να αιτηθείς ένα αρχείο των <strong>αναρτήσεων και ανεβασμένων πολυμέσων</strong> σου. Τα δεδομένα θα είναι σε μορφή ActivityPub, προσπελάσιμα από οποιοδήποτε συμβατό πρόγραμμα. Μπορείς να αιτηθείς ένα αρχείο κάθε 7 μέρες.
in_progress: Συγκεντρώνουμε το αρχείο σου... in_progress: Συγκεντρώνουμε το αρχείο σου...
request: Αιτήσου το αρχείο σου request: Αιτήσου το αρχείο σου
size: Μέγεθος size: Μέγεθος
@ -1668,7 +1668,7 @@ el:
table: table:
expires_at: Λήγει expires_at: Λήγει
uses: Χρήσεις uses: Χρήσεις
title: Προσκάλεσε κόσμο title: Προσκάλεσε άτομα
link_preview: link_preview:
author_html: Από %{name} author_html: Από %{name}
potentially_sensitive_content: potentially_sensitive_content:
@ -1706,26 +1706,26 @@ el:
missing_also_known_as: δεν είναι ψευδώνυμο αυτού του λογαριασμού missing_also_known_as: δεν είναι ψευδώνυμο αυτού του λογαριασμού
move_to_self: δεν μπορεί να είναι ο τρέχων λογαριασμός move_to_self: δεν μπορεί να είναι ο τρέχων λογαριασμός
not_found: δεν βρέθηκε not_found: δεν βρέθηκε
on_cooldown: Είσαι σε περίοδο ηρεμίας on_cooldown: Είσαι σε περίοδο αναμονής
followers_count: Ακόλουθοι τη στιγμή της μεταφοράς followers_count: Ακόλουθοι τη στιγμή της μεταφοράς
incoming_migrations: Μεταφορά από διαφορετικό λογαριασμό incoming_migrations: Μεταφορά από διαφορετικό λογαριασμό
incoming_migrations_html: Για να μετακομίσεις από έναν άλλο λογαριασμό σε αυτόν εδώ, πρώτα πρέπει να <a href="%{path}">δημιουργήσεις ένα ψευδώνυμο λογαριασμού</a>. incoming_migrations_html: Για να μεταφερθείς από έναν άλλο λογαριασμό σε αυτόν εδώ, πρώτα πρέπει να <a href="%{path}">δημιουργήσεις ένα ψευδώνυμο λογαριασμού</a>.
moved_msg: Ο λογαριασμός σου πλέον ανακατευθύνεται στον %{acct} και οι ακόλουθοί σου μεταφέρονται εκεί. moved_msg: Ο λογαριασμός σου τώρα ανακατευθύνεται στο %{acct} και οι ακόλουθοί σου μεταφέρονται εκεί.
not_redirecting: Ο λογαριασμός σου δεν ανακατευθύνεται σε κανέναν άλλο προς το παρόν. not_redirecting: Ο λογαριασμός σου δεν ανακατευθύνεται σε κανέναν άλλο προς το παρόν.
on_cooldown: Έχεις μετακομίσει το λογαριασμό σου πρόσφατα. Η δυνατότητα αυτή θα γίνει πάλι διαθέσιμη σε %{count} μέρες. on_cooldown: Έχεις μετακινήσει τον λογαριασμό σου πρόσφατα. Η δυνατότητα αυτή θα γίνει πάλι διαθέσιμη σε %{count} μέρες.
past_migrations: Προηγούμενες μετακινήσεις past_migrations: Προηγούμενες μετακινήσεις
proceed_with_move: Μετακίνηση ακολούθων proceed_with_move: Μεταφορά ακολούθων
redirected_msg: Ο λογαριασμός σου ανακατευθύνεται στον %{acct}. redirected_msg: Ο λογαριασμός σου τώρα ανακατευθύνεται στο %{acct}.
redirecting_to: Ο λογαριασμός σου ανακατευθύνεται στον %{acct}. redirecting_to: Ο λογαριασμός σου ανακατευθύνεται στο %{acct}.
set_redirect: Όρισε ανακατεύθυνση set_redirect: Όρισε ανακατεύθυνση
warning: warning:
backreference_required: Θα πρέπει πρώτα να ρυθμιστεί μια παραπομπή από τον νέο λογαριασμό προς αυτόν backreference_required: Θα πρέπει πρώτα να ρυθμιστεί μια παραπομπή από τον νέο λογαριασμό προς αυτόν
before: 'Πριν συνεχίσεις, παρακαλούμε διάβασε αυτές τις σημειώσεις προσεκτικά:' before: 'Πριν συνεχίσεις, παρακαλούμε διάβασε αυτές τις σημειώσεις προσεκτικά:'
cooldown: Μετά τη μετακίνηση υπάρχει μια περίοδος αναμονής κατά τη διάρκεια της οποίας δεν θα είσαι σε θέση να μετακινηθείς ξανά cooldown: Μετά τη μετακίνηση υπάρχει μια περίοδος αναμονής κατά τη διάρκεια της οποίας δεν θα είσαι σε θέση να μετακινηθείς ξανά
disabled_account: Ο τρέχων λογαριασμός σου δε θα είναι πλήρως ενεργός μετά. Πάντως θα έχεις πρόσβαση στην εξαγωγή δεδομένων καθώς και στην επανενεργοποίηση. disabled_account: Ο τρέχων λογαριασμός σου δε θα είναι πλήρως χρησιμοποιήσιμος μετά. Πάντως, θα έχεις πρόσβαση στην εξαγωγή δεδομένων καθώς και στην επανενεργοποίηση.
followers: Αυτή η ενέργεια θα μεταφέρει όλους τους ακόλουθούς σου από τον τρέχοντα λογαριασμό στον νέο λογαριασμό followers: Αυτή η ενέργεια θα μεταφέρει όλους τους ακόλουθούς σου από τον τρέχοντα λογαριασμό στον νέο λογαριασμό
only_redirect_html: Εναλλακτικά, μπορείς <a href="%{path}">απλά να προσθέσεις μια ανακατατεύθυνση στο προφίλ σου</a>. only_redirect_html: Εναλλακτικά, μπορείς <a href="%{path}">απλά να προσθέσεις μια ανακατατεύθυνση στο προφίλ σου</a>.
other_data: Δεν θα μετακινηθούν αυτόματα άλλα δεδομένα (συμπεριλαμβανομένου των αναρτήσεων σας και της λίστας των λογαριασμών που ακολουθείτε) other_data: Δεν θα μετακινηθούν αυτόματα άλλα δεδομένα (συμπεριλαμβανομένου των αναρτήσεων σου και της λίστας των λογαριασμών που ακολουθείς)
redirect: Το προφίλ του τρέχοντος λογαριασμού σου θα ενημερωθεί με μια σημείωση ανακατεύθυνσης και θα εξαιρεθεί από τα αποτελέσματα αναζητήσεων redirect: Το προφίλ του τρέχοντος λογαριασμού σου θα ενημερωθεί με μια σημείωση ανακατεύθυνσης και θα εξαιρεθεί από τα αποτελέσματα αναζητήσεων
moderation: moderation:
title: Συντονισμός title: Συντονισμός
@ -1944,7 +1944,7 @@ el:
featured_tags: Αναδεδειγμένες ετικέτες featured_tags: Αναδεδειγμένες ετικέτες
import: Εισαγωγή import: Εισαγωγή
import_and_export: Εισαγωγή και εξαγωγή import_and_export: Εισαγωγή και εξαγωγή
migrate: Μετακόμιση λογαριασμού migrate: Μετακίνηση λογαριασμού
notifications: Ειδοποιήσεις μέσω email notifications: Ειδοποιήσεις μέσω email
preferences: Προτιμήσεις preferences: Προτιμήσεις
profile: Προφίλ profile: Προφίλ
@ -2238,7 +2238,7 @@ el:
signed_in_as: 'Έχεις συνδεθεί ως:' signed_in_as: 'Έχεις συνδεθεί ως:'
verification: verification:
extra_instructions_html: <strong>Συμβουλή:</strong> Ο σύνδεσμος στην ιστοσελίδα σου μπορεί να είναι αόρατος. Το σημαντικό μέρος είναι το <code>rel="me"</code> που αποτρέπει την μίμηση σε ιστοσελίδες με περιεχόμενο παραγόμενο από χρήστες. Μπορείς ακόμα να χρησιμοποιήσεις μια ετικέτα <code>link</code> στην κεφαλίδα της σελίδας αντί για <code>a</code>, αλλά η HTML πρέπει να είναι προσβάσιμη χωρίς την εκτέλεση JavaScript. extra_instructions_html: <strong>Συμβουλή:</strong> Ο σύνδεσμος στην ιστοσελίδα σου μπορεί να είναι αόρατος. Το σημαντικό μέρος είναι το <code>rel="me"</code> που αποτρέπει την μίμηση σε ιστοσελίδες με περιεχόμενο παραγόμενο από χρήστες. Μπορείς ακόμα να χρησιμοποιήσεις μια ετικέτα <code>link</code> στην κεφαλίδα της σελίδας αντί για <code>a</code>, αλλά η HTML πρέπει να είναι προσβάσιμη χωρίς την εκτέλεση JavaScript.
here_is_how: Δείτε πώς here_is_how: Ορίστε πώς
hint_html: Η <strong>επαλήθευση της ταυτότητας στο Mastodon είναι για όλους.</strong> Βασισμένο σε ανοιχτά πρότυπα ιστού, τώρα και για πάντα δωρεάν. Το μόνο που χρειάζεσαι είναι μια προσωπική ιστοσελίδα που ο κόσμος να σε αναγνωρίζει από αυτή. Όταν βάζεις σύνδεσμο προς αυτήν την ιστοσελίδα από το προφίλ σου, θα ελέγξουμε ότι η ιστοσελίδα συνδέει πίσω στο προφίλ σου και θα δείξουμε μια οπτική ένδειξη σε αυτό. hint_html: Η <strong>επαλήθευση της ταυτότητας στο Mastodon είναι για όλους.</strong> Βασισμένο σε ανοιχτά πρότυπα ιστού, τώρα και για πάντα δωρεάν. Το μόνο που χρειάζεσαι είναι μια προσωπική ιστοσελίδα που ο κόσμος να σε αναγνωρίζει από αυτή. Όταν βάζεις σύνδεσμο προς αυτήν την ιστοσελίδα από το προφίλ σου, θα ελέγξουμε ότι η ιστοσελίδα συνδέει πίσω στο προφίλ σου και θα δείξουμε μια οπτική ένδειξη σε αυτό.
instructions_html: Αντέγραψε και επικόλλησε τον παρακάτω κώδικα στην HTML της ιστοσελίδας σου. Στη συνέχεια, πρόσθεσε τη διεύθυνση της ιστοσελίδας σου σε ένα από τα επιπλέον πεδία στο προφίλ σου από την καρτέλα "Επεξεργασία προφίλ" και αποθήκευσε τις αλλαγές. instructions_html: Αντέγραψε και επικόλλησε τον παρακάτω κώδικα στην HTML της ιστοσελίδας σου. Στη συνέχεια, πρόσθεσε τη διεύθυνση της ιστοσελίδας σου σε ένα από τα επιπλέον πεδία στο προφίλ σου από την καρτέλα "Επεξεργασία προφίλ" και αποθήκευσε τις αλλαγές.
verification: Επαλήθευση verification: Επαλήθευση

View File

@ -1440,11 +1440,11 @@ en:
one: Interact with this post and discover more like it. one: Interact with this post and discover more like it.
other: Interact with these posts and discover more. other: Interact with these posts and discover more.
subject: subject:
one: 'New post: "%{excerpt}"' plural: New posts from %{name}
other: New posts from %{name} singular: 'New post: "%{excerpt}"'
title: title:
one: 'New post: "%{excerpt}"' plural: New posts from %{name}
other: New posts from %{name} singular: 'New post: "%{excerpt}"'
email_subscriptions: email_subscriptions:
active: Active active: Active
confirmations: confirmations:

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