Merge remote-tracking branch 'parent/main' into upstream-20240531
This commit is contained in:
commit
13ad383039
101 changed files with 1486 additions and 1232 deletions
|
@ -111,11 +111,11 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def account_ids
|
||||
Array(accounts_params[:ids]).uniq.map(&:to_i)
|
||||
Array(accounts_params[:id]).uniq.map(&:to_i)
|
||||
end
|
||||
|
||||
def accounts_params
|
||||
params.permit(ids: [])
|
||||
params.permit(id: [])
|
||||
end
|
||||
|
||||
def account_params
|
||||
|
|
|
@ -38,15 +38,15 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||
def paginated_conversations
|
||||
AccountConversation.where(account: current_account)
|
||||
.includes(
|
||||
account: :account_stat,
|
||||
account: [:account_stat, user: :role],
|
||||
last_status: [
|
||||
:media_attachments,
|
||||
:status_stat,
|
||||
:tags,
|
||||
{
|
||||
preview_cards_status: :preview_card,
|
||||
active_mentions: [account: :account_stat],
|
||||
account: :account_stat,
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
active_mentions: :account,
|
||||
account: [:account_stat, user: :role],
|
||||
},
|
||||
]
|
||||
)
|
||||
|
|
|
@ -155,11 +155,11 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
end
|
||||
|
||||
def status_ids
|
||||
Array(statuses_params[:ids]).uniq.map(&:to_i)
|
||||
Array(statuses_params[:id]).uniq.map(&:to_i)
|
||||
end
|
||||
|
||||
def statuses_params
|
||||
params.permit(ids: [])
|
||||
params.permit(id: [])
|
||||
end
|
||||
|
||||
def status_params
|
||||
|
|
|
@ -44,7 +44,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
|
||||
def build_resource(hash = nil)
|
||||
super(hash)
|
||||
super
|
||||
|
||||
resource.locale = I18n.locale
|
||||
resource.invite_code = @invite&.code if resource.invite_code.blank?
|
||||
|
|
|
@ -65,7 +65,7 @@ window.addEventListener('message', (e) => {
|
|||
{
|
||||
type: 'setHeight',
|
||||
id: data.id,
|
||||
height: document.getElementsByTagName('html')[0].scrollHeight,
|
||||
height: document.getElementsByTagName('html')[0]?.scrollHeight,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
|
@ -135,7 +135,7 @@ function loaded() {
|
|||
);
|
||||
};
|
||||
const todayFormat = new IntlMessageFormat(
|
||||
localeData['relative_format.today'] || 'Today at {time}',
|
||||
localeData['relative_format.today'] ?? 'Today at {time}',
|
||||
locale,
|
||||
);
|
||||
|
||||
|
@ -288,13 +288,13 @@ function loaded() {
|
|||
if (statusEl.dataset.spoiler === 'expanded') {
|
||||
statusEl.dataset.spoiler = 'folded';
|
||||
this.textContent = new IntlMessageFormat(
|
||||
localeData['status.show_more'] || 'Show more',
|
||||
localeData['status.show_more'] ?? 'Show more',
|
||||
locale,
|
||||
).format() as string;
|
||||
} else {
|
||||
statusEl.dataset.spoiler = 'expanded';
|
||||
this.textContent = new IntlMessageFormat(
|
||||
localeData['status.show_less'] || 'Show less',
|
||||
localeData['status.show_less'] ?? 'Show less',
|
||||
locale,
|
||||
).format() as string;
|
||||
}
|
||||
|
@ -316,8 +316,8 @@ function loaded() {
|
|||
|
||||
const message =
|
||||
statusEl.dataset.spoiler === 'expanded'
|
||||
? localeData['status.show_less'] || 'Show less'
|
||||
: localeData['status.show_more'] || 'Show more';
|
||||
? localeData['status.show_less'] ?? 'Show less'
|
||||
: localeData['status.show_more'] ?? 'Show more';
|
||||
spoilerLink.textContent = new IntlMessageFormat(
|
||||
message,
|
||||
locale,
|
||||
|
|
|
@ -67,7 +67,9 @@ const fetchInteractionURLFailure = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const isValidDomain = (value: string) => {
|
||||
const isValidDomain = (value: unknown) => {
|
||||
if (typeof value !== 'string') return false;
|
||||
|
||||
const url = new URL('https:///path');
|
||||
url.hostname = value;
|
||||
return url.hostname === value;
|
||||
|
@ -124,6 +126,11 @@ const fromAcct = (acct: string) => {
|
|||
const domain = segments[1];
|
||||
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
|
||||
|
||||
if (!domain) {
|
||||
fetchInteractionURLFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
axios
|
||||
.get(`https://${domain}/.well-known/webfinger`, {
|
||||
params: { resource: `acct:${acct}` },
|
||||
|
|
|
@ -48,8 +48,9 @@ export const AnimatedNumber: React.FC<Props> = ({ value }) => {
|
|||
<span
|
||||
key={key}
|
||||
style={{
|
||||
position: direction * style.y > 0 ? 'absolute' : 'static',
|
||||
transform: `translateY(${style.y * 100}%)`,
|
||||
position:
|
||||
direction * (style.y ?? 0) > 0 ? 'absolute' : 'static',
|
||||
transform: `translateY(${(style.y ?? 0) * 100}%)`,
|
||||
}}
|
||||
>
|
||||
<ShortNumber value={data as number} />
|
||||
|
|
|
@ -52,7 +52,10 @@ function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
|
|||
);
|
||||
|
||||
return Object.values(groups).map((tags) => {
|
||||
if (tags.length === 1) return tags[0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know that the array has at least one element
|
||||
const firstTag = tags[0]!;
|
||||
|
||||
if (tags.length === 1) return firstTag;
|
||||
|
||||
// The best match is the one where we have the less difference between upper and lower case letter count
|
||||
const best = minBy(tags, (tag) => {
|
||||
|
@ -66,7 +69,7 @@ function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
|
|||
return Math.abs(lowerCase - upperCase);
|
||||
});
|
||||
|
||||
return best ?? tags[0];
|
||||
return best ?? firstTag;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ const ShortNumberCounter: React.FC<ShortNumberCounterProps> = ({ value }) => {
|
|||
|
||||
const count = (
|
||||
<FormattedNumber
|
||||
value={rawNumber}
|
||||
value={rawNumber ?? 0}
|
||||
maximumFractionDigits={maxFractionDigits}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -29,7 +29,10 @@ const emojis: Emojis = {};
|
|||
|
||||
// decompress
|
||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
||||
const [_filenameData, searchData] = shortCodesToEmojiData[shortCode];
|
||||
const emojiData = shortCodesToEmojiData[shortCode];
|
||||
if (!emojiData) return;
|
||||
|
||||
const [_filenameData, searchData] = emojiData;
|
||||
const [native, short_names, search, unified] = searchData;
|
||||
|
||||
emojis[shortCode] = {
|
||||
|
|
|
@ -46,7 +46,10 @@ function processEmojiMapData(
|
|||
Object.keys(shortCodesToEmojiData).forEach(
|
||||
(shortCode: ShortCodesToEmojiDataKey) => {
|
||||
if (shortCode === undefined) return;
|
||||
const [filenameData, _searchData] = shortCodesToEmojiData[shortCode];
|
||||
|
||||
const emojiData = shortCodesToEmojiData[shortCode];
|
||||
if (!emojiData) return;
|
||||
const [filenameData, _searchData] = emojiData;
|
||||
filenameData.forEach((emojiMapData) => {
|
||||
processEmojiMapData(emojiMapData, shortCode);
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ const mapStateToProps = (state, { columnId }) => {
|
|||
return {
|
||||
settings: columns.get(index).get('params'),
|
||||
onLoad (value) {
|
||||
return api(() => state).get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
|
||||
return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
|
||||
return (response.data.hashtags || []).map((tag) => {
|
||||
return { value: tag.name, label: `#${tag.name}` };
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@ import { PureComponent } from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
||||
import Immutable from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
@ -13,6 +15,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
|
||||
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
|
||||
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
|
@ -56,6 +59,20 @@ const addAutoPlay = html => {
|
|||
return html;
|
||||
};
|
||||
|
||||
const MoreFromAuthor = ({ author }) => (
|
||||
<div className='more-from-author'>
|
||||
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
|
||||
<use xlinkHref='#logo-symbol-icon' />
|
||||
</svg>
|
||||
|
||||
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <Link to={`/@${author.get('acct')}`}><Avatar account={author} size={16} /> {author.get('display_name')}</Link> }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
MoreFromAuthor.propTypes = {
|
||||
author: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
export default class Card extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
@ -136,6 +153,7 @@ export default class Card extends PureComponent {
|
|||
const interactive = card.get('type') === 'video';
|
||||
const language = card.get('language') || '';
|
||||
const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;
|
||||
const showAuthor = !!card.get('author_account');
|
||||
|
||||
const description = (
|
||||
<div className='status-card__content'>
|
||||
|
@ -146,7 +164,7 @@ export default class Card extends PureComponent {
|
|||
|
||||
<strong className='status-card__title' title={card.get('title')} lang={language}>{card.get('title')}</strong>
|
||||
|
||||
{card.get('author_name').length > 0 ? <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span> : <span className='status-card__description' lang={language}>{card.get('description')}</span>}
|
||||
{!showAuthor && (card.get('author_name').length > 0 ? <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span> : <span className='status-card__description' lang={language}>{card.get('description')}</span>)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -235,10 +253,14 @@ export default class Card extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<a href={card.get('url')} className={classNames('status-card', { expanded: largeImage })} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
|
||||
{embed}
|
||||
{description}
|
||||
</a>
|
||||
<>
|
||||
<a href={card.get('url')} className={classNames('status-card', { expanded: largeImage, bottomless: showAuthor })} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
|
||||
{embed}
|
||||
{description}
|
||||
</a>
|
||||
|
||||
{showAuthor && <MoreFromAuthor author={card.get('author_account')} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Mostra el perfil de totes maneres",
|
||||
"limited_account_hint.title": "Aquest perfil l'han amagat els moderadors de {domain}.",
|
||||
"link_preview.author": "Per {name}",
|
||||
"link_preview.more_from_author": "Més de {name}",
|
||||
"lists.account.add": "Afegeix a la llista",
|
||||
"lists.account.remove": "Elimina de la llista",
|
||||
"lists.delete": "Elimina la llista",
|
||||
|
|
|
@ -520,6 +520,7 @@
|
|||
"limited_account_hint.action": "Show profile anyway",
|
||||
"limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.",
|
||||
"link_preview.author": "By {name}",
|
||||
"link_preview.more_from_author": "More from {name}",
|
||||
"lists.account.add": "Add to list",
|
||||
"lists.account.remove": "Remove from list",
|
||||
"lists.antennas": "Related antennas",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Mostrar perfil de todos modos",
|
||||
"limited_account_hint.title": "Este perfil fue ocultado por los moderadores de {domain}.",
|
||||
"link_preview.author": "Por {name}",
|
||||
"link_preview.more_from_author": "Más de {name}",
|
||||
"lists.account.add": "Agregar a lista",
|
||||
"lists.account.remove": "Quitar de lista",
|
||||
"lists.delete": "Eliminar lista",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Näytä profiili joka tapauksessa",
|
||||
"limited_account_hint.title": "Palvelimen {domain} valvojat ovat piilottaneet tämän käyttäjätilin.",
|
||||
"link_preview.author": "Julkaissut {name}",
|
||||
"link_preview.more_from_author": "Lisää käyttäjältä {name}",
|
||||
"lists.account.add": "Lisää listalle",
|
||||
"lists.account.remove": "Poista listalta",
|
||||
"lists.delete": "Poista lista",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Mostrar perfil igualmente",
|
||||
"limited_account_hint.title": "Este perfil foi agochado pola moderación de {domain}.",
|
||||
"link_preview.author": "Por {name}",
|
||||
"link_preview.more_from_author": "Máis de {name}",
|
||||
"lists.account.add": "Engadir á listaxe",
|
||||
"lists.account.remove": "Eliminar da listaxe",
|
||||
"lists.delete": "Eliminar listaxe",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Mostra comunque il profilo",
|
||||
"limited_account_hint.title": "Questo profilo è stato nascosto dai moderatori di {domain}.",
|
||||
"link_preview.author": "Di {name}",
|
||||
"link_preview.more_from_author": "Altro da {name}",
|
||||
"lists.account.add": "Aggiungi all'elenco",
|
||||
"lists.account.remove": "Rimuovi dall'elenco",
|
||||
"lists.delete": "Elimina elenco",
|
||||
|
|
|
@ -234,7 +234,7 @@
|
|||
"embed.preview": "이렇게 표시됩니다:",
|
||||
"emoji_button.activity": "활동",
|
||||
"emoji_button.clear": "지우기",
|
||||
"emoji_button.custom": "사용자 지정",
|
||||
"emoji_button.custom": "커스텀",
|
||||
"emoji_button.flags": "깃발",
|
||||
"emoji_button.food": "음식과 마실것",
|
||||
"emoji_button.label": "에모지 추가",
|
||||
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "그래도 프로필 보기",
|
||||
"limited_account_hint.title": "이 프로필은 {domain}의 중재자에 의해 숨겨진 상태입니다.",
|
||||
"link_preview.author": "{name}",
|
||||
"link_preview.more_from_author": "{name} 더 둘러보기",
|
||||
"lists.account.add": "리스트에 추가",
|
||||
"lists.account.remove": "리스트에서 제거",
|
||||
"lists.delete": "리스트 삭제",
|
||||
|
|
|
@ -466,6 +466,7 @@
|
|||
"notification.follow_request": "{name} paprašė tave sekti",
|
||||
"notification.mention": "{name} paminėjo tave",
|
||||
"notification.moderation-warning.learn_more": "Sužinoti daugiau",
|
||||
"notification.moderation_warning": "Gavai prižiūrėjimo įspėjimą",
|
||||
"notification.moderation_warning.action_delete_statuses": "Kai kurie tavo įrašai buvo pašalintos.",
|
||||
"notification.moderation_warning.action_disable": "Tavo paskyra buvo išjungta.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Kai kurie tavo įrašai buvo pažymėtos kaip jautrios.",
|
||||
|
|
|
@ -297,6 +297,7 @@
|
|||
"filter_modal.select_filter.subtitle": "Bruk ein eksisterande kategori eller opprett ein ny",
|
||||
"filter_modal.select_filter.title": "Filtrer dette innlegget",
|
||||
"filter_modal.title.status": "Filtrer eit innlegg",
|
||||
"filtered_notifications_banner.mentions": "{count, plural, one {omtale} other {omtaler}}",
|
||||
"filtered_notifications_banner.pending_requests": "Varsel frå {count, plural, =0 {ingen} one {ein person} other {# folk}} du kanskje kjenner",
|
||||
"filtered_notifications_banner.title": "Filtrerte varslingar",
|
||||
"firehose.all": "Alle",
|
||||
|
@ -307,6 +308,8 @@
|
|||
"follow_requests.unlocked_explanation": "Sjølv om kontoen din ikkje er låst tenkte dei som driv {domain} at du kanskje ville gå gjennom førespurnadar frå desse kontoane manuelt.",
|
||||
"follow_suggestions.curated_suggestion": "Utvalt av staben",
|
||||
"follow_suggestions.dismiss": "Ikkje vis igjen",
|
||||
"follow_suggestions.featured_longer": "Hanplukka av gjengen på {domain}",
|
||||
"follow_suggestions.friends_of_friends_longer": "Populært hjå dei du fylgjer",
|
||||
"follow_suggestions.hints.featured": "Denne profilen er handplukka av folka på {domain}.",
|
||||
"follow_suggestions.hints.friends_of_friends": "Denne profilen er populær hjå dei du fylgjer.",
|
||||
"follow_suggestions.hints.most_followed": "Mange på {domain} fylgjer denne profilen.",
|
||||
|
@ -314,6 +317,8 @@
|
|||
"follow_suggestions.hints.similar_to_recently_followed": "Denne profilen liknar på dei andre profilane du har fylgt i det siste.",
|
||||
"follow_suggestions.personalized_suggestion": "Personleg forslag",
|
||||
"follow_suggestions.popular_suggestion": "Populært forslag",
|
||||
"follow_suggestions.popular_suggestion_longer": "Populært på {domain}",
|
||||
"follow_suggestions.similar_to_recently_followed_longer": "Liknar på profilar du har fylgt i det siste",
|
||||
"follow_suggestions.view_all": "Vis alle",
|
||||
"follow_suggestions.who_to_follow": "Kven du kan fylgja",
|
||||
"followed_tags": "Fylgde emneknaggar",
|
||||
|
@ -668,7 +673,7 @@
|
|||
"search.quick_action.account_search": "Profiler som samsvarer med {x}",
|
||||
"search.quick_action.go_to_account": "Gå til profil {x}",
|
||||
"search.quick_action.go_to_hashtag": "Gå til emneknagg {x}",
|
||||
"search.quick_action.open_url": "Åpne URL i Mastodon",
|
||||
"search.quick_action.open_url": "Opne adressa i Mastodon",
|
||||
"search.quick_action.status_search": "Innlegg som samsvarer med {x}",
|
||||
"search.search_or_paste": "Søk eller lim inn URL",
|
||||
"search_popout.full_text_search_disabled_message": "Ikkje tilgjengeleg på {domain}.",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Pokaż profil mimo to",
|
||||
"limited_account_hint.title": "Ten profil został ukryty przez moderatorów {domain}.",
|
||||
"link_preview.author": "{name}",
|
||||
"link_preview.more_from_author": "Więcej od {name}",
|
||||
"lists.account.add": "Dodaj do listy",
|
||||
"lists.account.remove": "Usunąć z listy",
|
||||
"lists.delete": "Usuń listę",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Exibir perfil mesmo assim",
|
||||
"limited_account_hint.title": "Este perfil foi ocultado pelos moderadores do {domain}.",
|
||||
"link_preview.author": "Por {name}",
|
||||
"link_preview.more_from_author": "Mais de {name}",
|
||||
"lists.account.add": "Adicionar à lista",
|
||||
"lists.account.remove": "Remover da lista",
|
||||
"lists.delete": "Excluir lista",
|
||||
|
@ -474,6 +475,7 @@
|
|||
"notification.follow_request": "{name} quer te seguir",
|
||||
"notification.mention": "{name} te mencionou",
|
||||
"notification.moderation-warning.learn_more": "Aprender mais",
|
||||
"notification.moderation_warning": "Você recebeu um aviso de moderação",
|
||||
"notification.moderation_warning.action_delete_statuses": "Algumas das suas publicações foram removidas.",
|
||||
"notification.moderation_warning.action_disable": "Sua conta foi desativada.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Algumas de suas publicações foram marcadas por ter conteúdo sensível.",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Exibir perfil mesmo assim",
|
||||
"limited_account_hint.title": "Este perfil foi ocultado pelos moderadores de {domain}.",
|
||||
"link_preview.author": "Por {name}",
|
||||
"link_preview.more_from_author": "Mais de {name}",
|
||||
"lists.account.add": "Adicionar à lista",
|
||||
"lists.account.remove": "Remover da lista",
|
||||
"lists.delete": "Eliminar lista",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"account.edit_profile": "පැතිකඩ සංස්කරණය",
|
||||
"account.enable_notifications": "@{name} පළ කරන විට මට දැනුම් දෙන්න",
|
||||
"account.endorse": "පැතිකඩෙහි විශේෂාංගය",
|
||||
"account.featured_tags.last_status_at": "අවසාන ලිපිය: {date}",
|
||||
"account.featured_tags.last_status_never": "ලිපි නැත",
|
||||
"account.follow": "අනුගමනය",
|
||||
"account.followers": "අනුගාමිකයින්",
|
||||
|
@ -104,6 +105,7 @@
|
|||
"compose_form.poll.duration": "මත විමසීමේ කාලය",
|
||||
"compose_form.poll.switch_to_multiple": "තේරීම් කිහිපයකට මත විමසුම වෙනස් කරන්න",
|
||||
"compose_form.poll.switch_to_single": "තනි තේරීමකට මත විමසුම වෙනස් කරන්න",
|
||||
"compose_form.publish": "ප්රකාශනය",
|
||||
"compose_form.publish_form": "නව ලිපිය",
|
||||
"compose_form.spoiler.marked": "අන්තර්ගත අවවාදය ඉවත් කරන්න",
|
||||
"compose_form.spoiler.unmarked": "අන්තර්ගත අවවාදයක් එක් කරන්න",
|
||||
|
@ -154,6 +156,7 @@
|
|||
"empty_column.bookmarked_statuses": "ඔබ සතුව පොත්යොමු තබන ලද ලිපි කිසිවක් නැත. ඔබ පොත්යොමුවක් තබන විට, එය මෙහි දිස්වනු ඇත.",
|
||||
"empty_column.domain_blocks": "අවහිර කරන ලද වසම් නැත.",
|
||||
"empty_column.explore_statuses": "දැන් කිසිවක් නැඹුරු නොවේ. පසුව නැවත පරීක්ෂා කරන්න!",
|
||||
"empty_column.favourited_statuses": "ඔබ සතුව ප්රියතම ලිපි කිසිවක් නැත. ඔබ යමකට ප්රිය කළ විට එය මෙහි පෙන්වනු ඇත.",
|
||||
"empty_column.follow_requests": "ඔබට තවමත් අනුගමන ඉල්ලීම් ලැබී නැත. ඉල්ලීමක් ලැබුණු විට, එය මෙහි පෙන්වනු ඇත.",
|
||||
"empty_column.home": "මුල් පිටුව හිස් ය! මෙය පිරවීමට බොහෝ පුද්ගලයින් අනුගමනය කරන්න.",
|
||||
"empty_column.lists": "ඔබට තවමත් ලැයිස්තු කිසිවක් නැත. ඔබ එකක් සාදන විට, එය මෙහි පෙන්වනු ඇත.",
|
||||
|
@ -205,6 +208,7 @@
|
|||
"interaction_modal.on_this_server": "මෙම සේවාදායකයෙහි",
|
||||
"interaction_modal.title.favourite": "{name}ගේ ලිපිය ප්රිය කරන්න",
|
||||
"interaction_modal.title.follow": "{name} අනුගමනය",
|
||||
"interaction_modal.title.reply": "{name}ගේ ලිපියට පිළිතුරු",
|
||||
"intervals.full.days": "{number, plural, one {දවස් #} other {දවස් #}}",
|
||||
"intervals.full.hours": "{number, plural, one {පැය #} other {පැය #}}",
|
||||
"intervals.full.minutes": "{number, plural, one {විනාඩි #} other {විනාඩි #}}",
|
||||
|
@ -239,6 +243,7 @@
|
|||
"lists.delete": "ලැයිස්තුව මකන්න",
|
||||
"lists.edit": "ලැයිස්තුව සංස්කරණය",
|
||||
"lists.edit.submit": "සිරැසිය සංශෝධනය",
|
||||
"lists.new.create": "එකතු",
|
||||
"lists.new.title_placeholder": "නව ලැයිස්තුවේ සිරැසිය",
|
||||
"lists.replies_policy.list": "ලැයිස්තුවේ සාමාජිකයින්",
|
||||
"lists.replies_policy.none": "කිසිවෙක් නැත",
|
||||
|
@ -266,6 +271,7 @@
|
|||
"navigation_bar.search": "සොයන්න",
|
||||
"navigation_bar.security": "ආරක්ෂාව",
|
||||
"not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
|
||||
"notification.favourite": "{name} ඔබගේ ලිපියට ප්රිය කළා",
|
||||
"notification.follow": "{name} ඔබව අනුගමනය කළා",
|
||||
"notification.mention": "{name} ඔබව සඳහන් කර ඇත",
|
||||
"notification.own_poll": "ඔබගේ මත විමසුම නිමයි",
|
||||
|
@ -395,6 +401,7 @@
|
|||
"status.admin_status": "මෙම ලිපිය මැදිහත්කරණ අතුරුමුහුණතෙහි අරින්න",
|
||||
"status.block": "@{name} අවහිර",
|
||||
"status.bookmark": "පොත්යොමුවක්",
|
||||
"status.copy": "ලිපියට සබැඳියේ පිටපතක්",
|
||||
"status.delete": "මකන්න",
|
||||
"status.detailed_status": "විස්තරාත්මක සංවාද දැක්ම",
|
||||
"status.edit": "සංස්කරණය",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Vseeno pokaži profil",
|
||||
"limited_account_hint.title": "Profil so moderatorji strežnika {domain} skrili.",
|
||||
"link_preview.author": "Avtor_ica {name}",
|
||||
"link_preview.more_from_author": "Več od {name}",
|
||||
"lists.account.add": "Dodaj na seznam",
|
||||
"lists.account.remove": "Odstrani s seznama",
|
||||
"lists.delete": "Izbriši seznam",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "Vẫn cứ xem",
|
||||
"limited_account_hint.title": "Người này đã bị ẩn bởi quản trị viên của {domain}.",
|
||||
"link_preview.author": "Bởi {name}",
|
||||
"link_preview.more_from_author": "Thêm từ {name}",
|
||||
"lists.account.add": "Thêm vào danh sách",
|
||||
"lists.account.remove": "Xóa khỏi danh sách",
|
||||
"lists.delete": "Xóa danh sách",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "仍要显示个人资料",
|
||||
"limited_account_hint.title": "此账号资料已被 {domain} 管理员隐藏。",
|
||||
"link_preview.author": "由 {name}",
|
||||
"link_preview.more_from_author": "查看 {name} 的更多内容",
|
||||
"lists.account.add": "添加到列表",
|
||||
"lists.account.remove": "从列表中移除",
|
||||
"lists.delete": "删除列表",
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"limited_account_hint.action": "一律顯示個人檔案",
|
||||
"limited_account_hint.title": "此個人檔案已被 {domain} 的管理員隱藏。",
|
||||
"link_preview.author": "來自 {name}",
|
||||
"link_preview.more_from_author": "來自 {name} 之更多內容",
|
||||
"lists.account.add": "新增至列表",
|
||||
"lists.account.remove": "自列表中移除",
|
||||
"lists.delete": "刪除列表",
|
||||
|
|
|
@ -74,8 +74,9 @@ export const soundsMiddleware = (): Middleware<
|
|||
if (isActionWithMetaSound(action)) {
|
||||
const sound = action.meta.sound;
|
||||
|
||||
if (sound && Object.hasOwn(soundCache, sound)) {
|
||||
play(soundCache[sound]);
|
||||
if (sound) {
|
||||
const s = soundCache[sound];
|
||||
if (s) play(s);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -89,21 +89,17 @@ type OnData<LoadDataResult, ReturnedData> = (
|
|||
},
|
||||
) => ReturnedData | DiscardLoadData | Promise<ReturnedData | DiscardLoadData>;
|
||||
|
||||
type ArgsType = Record<string, unknown> | undefined;
|
||||
|
||||
// Overload when there is no `onData` method, the payload is the `onData` result
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
>(
|
||||
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
|
||||
name: string,
|
||||
loadData: (args: Args) => Promise<LoadDataResult>,
|
||||
thunkOptions?: AppThunkOptions,
|
||||
): ReturnType<typeof createThunk<Args, LoadDataResult>>;
|
||||
|
||||
// Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
>(
|
||||
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
|
||||
name: string,
|
||||
loadData: (args: Args) => Promise<LoadDataResult>,
|
||||
onDataOrThunkOptions?:
|
||||
|
@ -113,10 +109,7 @@ export function createDataLoadingThunk<
|
|||
): ReturnType<typeof createThunk<Args, void>>;
|
||||
|
||||
// Overload when the `onData` method returns nothing, then the mayload is the `onData` result
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
>(
|
||||
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
|
||||
name: string,
|
||||
loadData: (args: Args) => Promise<LoadDataResult>,
|
||||
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, void>,
|
||||
|
@ -126,7 +119,7 @@ export function createDataLoadingThunk<
|
|||
// Overload when there is an `onData` method returning something
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
Args extends ArgsType,
|
||||
Returned,
|
||||
>(
|
||||
name: string,
|
||||
|
@ -162,7 +155,7 @@ export function createDataLoadingThunk<
|
|||
*/
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
Args extends ArgsType,
|
||||
Returned,
|
||||
>(
|
||||
name: string,
|
||||
|
|
|
@ -4040,6 +4040,10 @@ button.icon-button.active i.fa-retweet {
|
|||
border: 1px solid var(--background-border-color);
|
||||
border-radius: 8px;
|
||||
|
||||
&.bottomless {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
|
@ -10654,3 +10658,42 @@ noscript {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.more-from-author {
|
||||
font-size: 14px;
|
||||
color: $darker-text-color;
|
||||
background: var(--surface-background-color);
|
||||
border: 1px solid var(--background-border-color);
|
||||
border-top: 0;
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.logo {
|
||||
height: 16px;
|
||||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
color: $primary-text-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
def process_status_params
|
||||
@status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url, object: @object, account: @account, friend_domain: friend_domain?)
|
||||
|
||||
attachment_ids = process_attachments.take(MediaAttachment::ACTIVITYPUB_STATUS_ATTACHMENT_MAX).map(&:id)
|
||||
attachment_ids = process_attachments.take(Status::MEDIA_ATTACHMENTS_LIMIT_FROM_REMOTE).map(&:id)
|
||||
|
||||
@params = {
|
||||
uri: @status_parser.uri,
|
||||
|
@ -341,7 +341,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
as_array(@object['attachment']).each do |attachment|
|
||||
media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment)
|
||||
|
||||
next if media_attachment_parser.remote_url.blank? || media_attachments.size >= MediaAttachment::ACTIVITYPUB_STATUS_ATTACHMENT_MAX
|
||||
next if media_attachment_parser.remote_url.blank? || media_attachments.size >= Status::MEDIA_ATTACHMENTS_LIMIT_FROM_REMOTE
|
||||
|
||||
begin
|
||||
media_attachment = MediaAttachment.create(
|
||||
|
|
|
@ -33,6 +33,6 @@ class ActivityPub::Serializer < ActiveModel::Serializer
|
|||
adapter_options[:named_contexts].merge!(_named_contexts)
|
||||
adapter_options[:context_extensions].merge!(_context_extensions)
|
||||
end
|
||||
super(adapter_options, options, adapter_instance)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ require_relative 'shared_timed_stack'
|
|||
|
||||
class ConnectionPool::SharedConnectionPool < ConnectionPool
|
||||
def initialize(options = {}, &block)
|
||||
super(options, &block)
|
||||
super
|
||||
|
||||
@available = ConnectionPool::SharedTimedStack.new(@size, &block)
|
||||
end
|
||||
|
|
|
@ -195,6 +195,10 @@ class LinkDetailsExtractor
|
|||
structured_data&.author_url
|
||||
end
|
||||
|
||||
def author_account
|
||||
opengraph_tag('fediverse:creator')
|
||||
end
|
||||
|
||||
def embed_url
|
||||
valid_url_or_nil(opengraph_tag('twitter:player:stream'))
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class RSS::Channel < RSS::Element
|
||||
def initialize
|
||||
super()
|
||||
super
|
||||
|
||||
@root = create_element('channel')
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class RSS::Item < RSS::Element
|
||||
def initialize
|
||||
super()
|
||||
super
|
||||
|
||||
@root = create_element('item')
|
||||
end
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Vacuum::ApplicationsVacuum
|
||||
def perform
|
||||
Doorkeeper::Application.where(owner_id: nil)
|
||||
.where.missing(:created_users, :access_tokens, :access_grants)
|
||||
.where(created_at: ...1.day.ago)
|
||||
.in_batches.delete_all
|
||||
end
|
||||
end
|
|
@ -150,6 +150,8 @@ class Account < ApplicationRecord
|
|||
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
|
||||
scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
|
||||
scope :dormant, -> { joins(:account_stat).merge(AccountStat.without_recent_activity) }
|
||||
scope :with_username, ->(value) { where arel_table[:username].lower.eq(value.to_s.downcase) }
|
||||
scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) }
|
||||
|
||||
after_update_commit :trigger_update_webhooks
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class AccountSuggestions::SettingSource < AccountSuggestions::Source
|
||||
def get(account, limit: DEFAULT_LIMIT)
|
||||
if setting_enabled?
|
||||
base_account_scope(account).where(setting_to_where_condition).limit(limit).pluck(:id).zip([key].cycle)
|
||||
base_account_scope(account).merge(setting_to_where_condition).limit(limit).pluck(:id).zip([key].cycle)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
@ -25,11 +25,9 @@ class AccountSuggestions::SettingSource < AccountSuggestions::Source
|
|||
|
||||
def setting_to_where_condition
|
||||
usernames_and_domains.map do |(username, domain)|
|
||||
Arel::Nodes::Grouping.new(
|
||||
Account.arel_table[:username].lower.eq(username.downcase).and(
|
||||
Account.arel_table[:domain].lower.eq(domain&.downcase)
|
||||
)
|
||||
)
|
||||
Account
|
||||
.with_username(username)
|
||||
.with_domain(domain)
|
||||
end.reduce(:or)
|
||||
end
|
||||
|
||||
|
|
|
@ -25,42 +25,11 @@ module Account::FinderConcern
|
|||
end
|
||||
|
||||
def find_remote(username, domain)
|
||||
AccountFinder.new(username, domain).account
|
||||
end
|
||||
end
|
||||
|
||||
class AccountFinder
|
||||
attr_reader :username, :domain
|
||||
|
||||
def initialize(username, domain)
|
||||
@username = username
|
||||
@domain = domain
|
||||
end
|
||||
|
||||
def account
|
||||
scoped_accounts.order(id: :asc).take
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scoped_accounts
|
||||
Account.unscoped.tap do |scope|
|
||||
scope.merge! with_usernames
|
||||
scope.merge! matching_username
|
||||
scope.merge! matching_domain
|
||||
end
|
||||
end
|
||||
|
||||
def with_usernames
|
||||
Account.where.not(Account.arel_table[:username].lower.eq '')
|
||||
end
|
||||
|
||||
def matching_username
|
||||
Account.where(Account.arel_table[:username].lower.eq username.to_s.downcase)
|
||||
end
|
||||
|
||||
def matching_domain
|
||||
Account.where(Account.arel_table[:domain].lower.eq(domain.nil? ? nil : domain.to_s.downcase))
|
||||
Account
|
||||
.with_username(username)
|
||||
.with_domain(domain)
|
||||
.order(id: :asc)
|
||||
.take
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ module Attachmentable
|
|||
|
||||
included do
|
||||
def self.has_attached_file(name, options = {}) # rubocop:disable Naming/PredicateName
|
||||
super(name, options)
|
||||
super
|
||||
|
||||
send(:"before_#{name}_validate", prepend: true) do
|
||||
attachment = send(name)
|
||||
|
|
|
@ -22,7 +22,7 @@ module User::LdapAuthenticable
|
|||
safe_username = safe_username.gsub(keys, replacement)
|
||||
end
|
||||
|
||||
resource = joins(:account).merge(Account.where(Account.arel_table[:username].lower.eq safe_username.downcase)).take
|
||||
resource = joins(:account).merge(Account.with_username(safe_username)).take
|
||||
|
||||
if resource.blank?
|
||||
resource = new(
|
||||
|
|
|
@ -35,10 +35,6 @@ class MediaAttachment < ApplicationRecord
|
|||
include Attachmentable
|
||||
include RoutingHelper
|
||||
|
||||
LOCAL_STATUS_ATTACHMENT_MAX = 4
|
||||
LOCAL_STATUS_ATTACHMENT_MAX_WITH_POLL = 4
|
||||
ACTIVITYPUB_STATUS_ATTACHMENT_MAX = 16
|
||||
|
||||
enum :type, { image: 0, gifv: 1, video: 2, unknown: 3, audio: 4 }
|
||||
enum :processing, { queued: 0, in_progress: 1, complete: 2, failed: 3 }, prefix: true
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
# published_at :datetime
|
||||
# image_description :string default(""), not null
|
||||
# image_file_size :integer
|
||||
# author_account_id :bigint(8)
|
||||
#
|
||||
|
||||
class PreviewCard < ApplicationRecord
|
||||
|
@ -54,6 +55,7 @@ class PreviewCard < ApplicationRecord
|
|||
has_many :statuses, through: :preview_cards_statuses
|
||||
|
||||
has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy
|
||||
belongs_to :author_account, class_name: 'Account', optional: true
|
||||
|
||||
has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, validate_media_type: false
|
||||
|
||||
|
|
|
@ -47,6 +47,10 @@ class Status < ApplicationRecord
|
|||
include Status::ThreadingConcern
|
||||
include DtlHelper
|
||||
|
||||
MEDIA_ATTACHMENTS_LIMIT = 4
|
||||
MEDIA_ATTACHMENTS_LIMIT_WITH_POLL = 4
|
||||
MEDIA_ATTACHMENTS_LIMIT_FROM_REMOTE = 16
|
||||
|
||||
rate_limit by: :account, family: :statuses
|
||||
|
||||
self.discard_column = :deleted_at
|
||||
|
@ -193,9 +197,9 @@ class Status < ApplicationRecord
|
|||
:reference_objects,
|
||||
:references,
|
||||
:scheduled_expiration_status,
|
||||
preview_cards_status: [:preview_card],
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
account: [:account_stat, user: :role],
|
||||
active_mentions: { account: :account_stat },
|
||||
active_mentions: :account,
|
||||
reblog: [
|
||||
:application,
|
||||
:tags,
|
||||
|
@ -205,7 +209,7 @@ class Status < ApplicationRecord
|
|||
:preloadable_poll,
|
||||
:reference_objects,
|
||||
:scheduled_expiration_status,
|
||||
preview_cards_status: [:preview_card],
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
account: [:account_stat, user: :role],
|
||||
active_mentions: { account: :account_stat },
|
||||
],
|
||||
|
@ -218,11 +222,11 @@ class Status < ApplicationRecord
|
|||
:preloadable_poll,
|
||||
:reference_objects,
|
||||
:scheduled_expiration_status,
|
||||
preview_cards_status: [:preview_card],
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
account: [:account_stat, user: :role],
|
||||
active_mentions: { account: :account_stat },
|
||||
active_mentions: :account,
|
||||
],
|
||||
thread: { account: :account_stat }
|
||||
thread: :account
|
||||
|
||||
delegate :domain, to: :account, prefix: true
|
||||
|
||||
|
@ -395,10 +399,6 @@ class Status < ApplicationRecord
|
|||
update_status_stat!(key => [public_send(key) - 1, 0].max)
|
||||
end
|
||||
|
||||
def add_status_referred_by_count!(diff)
|
||||
update_status_stat!(status_referred_by_count: [public_send(:status_referred_by_count) + diff, 0].max)
|
||||
end
|
||||
|
||||
def emoji_reactions_grouped_by_name(account = nil, **options)
|
||||
return [] if account.present? && !self.account.show_emoji_reaction?(account)
|
||||
return [] if account.nil? && !options[:force] && self.account.emoji_reaction_policy != :allow
|
||||
|
|
|
@ -61,9 +61,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||
|
||||
statuses: {
|
||||
max_characters: StatusLengthValidator::MAX_CHARS,
|
||||
max_media_attachments: MediaAttachment::LOCAL_STATUS_ATTACHMENT_MAX,
|
||||
max_media_attachments_with_poll: MediaAttachment::LOCAL_STATUS_ATTACHMENT_MAX_WITH_POLL,
|
||||
max_media_attachments_from_activitypub: MediaAttachment::ACTIVITYPUB_STATUS_ATTACHMENT_MAX,
|
||||
max_media_attachments: Status::MEDIA_ATTACHMENTS_LIMIT,
|
||||
max_media_attachments_with_poll: Status::MEDIA_ATTACHMENTS_LIMIT_WITH_POLL,
|
||||
max_media_attachments_from_activitypub: Status::MEDIA_ATTACHMENTS_LIMIT_FROM_REMOTE,
|
||||
characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS,
|
||||
},
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
|
|||
:provider_url, :html, :width, :height,
|
||||
:image, :image_description, :embed_url, :blurhash, :published_at
|
||||
|
||||
has_one :author_account, serializer: REST::AccountSerializer, if: -> { object.author_account.present? }
|
||||
|
||||
def url
|
||||
object.original_url.presence || object.url
|
||||
end
|
||||
|
|
|
@ -66,9 +66,9 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
|
|||
|
||||
statuses: {
|
||||
max_characters: StatusLengthValidator::MAX_CHARS,
|
||||
max_media_attachments: MediaAttachment::LOCAL_STATUS_ATTACHMENT_MAX,
|
||||
max_media_attachments_with_poll: MediaAttachment::LOCAL_STATUS_ATTACHMENT_MAX_WITH_POLL,
|
||||
max_media_attachments_from_activitypub: MediaAttachment::ACTIVITYPUB_STATUS_ATTACHMENT_MAX,
|
||||
max_media_attachments: Status::MEDIA_ATTACHMENTS_LIMIT,
|
||||
max_media_attachments_with_poll: Status::MEDIA_ATTACHMENTS_LIMIT_WITH_POLL,
|
||||
max_media_attachments_from_activitypub: Status::MEDIA_ATTACHMENTS_LIMIT_FROM_REMOTE,
|
||||
characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS,
|
||||
},
|
||||
|
||||
|
|
|
@ -174,13 +174,23 @@ class AccountSearchService < BaseService
|
|||
end
|
||||
|
||||
def call(query, account = nil, options = {})
|
||||
@query = query&.strip&.gsub(/\A@/, '')
|
||||
@limit = options[:limit].to_i
|
||||
@offset = options[:offset].to_i
|
||||
@options = options
|
||||
@account = account
|
||||
MastodonOTELTracer.in_span('AccountSearchService#call') do |span|
|
||||
@query = query&.strip&.gsub(/\A@/, '')
|
||||
@limit = options[:limit].to_i
|
||||
@offset = options[:offset].to_i
|
||||
@options = options
|
||||
@account = account
|
||||
|
||||
search_service_results.compact.uniq
|
||||
span.add_attributes(
|
||||
'search.offset' => @offset,
|
||||
'search.limit' => @limit,
|
||||
'search.backend' => Chewy.enabled? ? 'elasticsearch' : 'database'
|
||||
)
|
||||
|
||||
search_service_results.compact.uniq.tap do |results|
|
||||
span.set_attribute('search.results.count', results.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -84,7 +84,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
as_array(@json['attachment']).each do |attachment|
|
||||
media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment)
|
||||
|
||||
next if media_attachment_parser.remote_url.blank? || @next_media_attachments.size > MediaAttachment::ACTIVITYPUB_STATUS_ATTACHMENT_MAX
|
||||
next if media_attachment_parser.remote_url.blank? || @next_media_attachments.size > Status::MEDIA_ATTACHMENTS_LIMIT_FROM_REMOTE
|
||||
|
||||
begin
|
||||
media_attachment = previous_media_attachments.find { |previous_media_attachment| previous_media_attachment.remote_url == media_attachment_parser.remote_url }
|
||||
|
|
|
@ -169,9 +169,12 @@ class FetchLinkCardService < BaseService
|
|||
return if html.nil?
|
||||
|
||||
link_details_extractor = LinkDetailsExtractor.new(@url, @html, @html_charset)
|
||||
provider = PreviewCardProvider.matching_domain(Addressable::URI.parse(link_details_extractor.canonical_url).normalized_host)
|
||||
linked_account = ResolveAccountService.new.call(link_details_extractor.author_account, suppress_errors: true) if link_details_extractor.author_account.present? && provider&.trendable?
|
||||
|
||||
@card = PreviewCard.find_or_initialize_by(url: link_details_extractor.canonical_url) if link_details_extractor.canonical_url != @card.url
|
||||
@card.assign_attributes(link_details_extractor.to_preview_card_attributes)
|
||||
@card.author_account = linked_account
|
||||
@card.save_with_optional_image! unless @card.title.blank? && @card.html.blank?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -150,6 +150,9 @@ class NotifyService < BaseService
|
|||
end
|
||||
|
||||
def statuses_that_mention_sender
|
||||
# This queries private mentions from the recipient to the sender up in the thread.
|
||||
# This allows up to 100 messages that do not match in the thread, allowing conversations
|
||||
# involving multiple people.
|
||||
Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @sender.id, depth_limit: 100])
|
||||
WITH RECURSIVE ancestors(id, in_reply_to_id, mention_id, path, depth) AS (
|
||||
SELECT s.id, s.in_reply_to_id, m.id, ARRAY[s.id], 0
|
||||
|
@ -157,16 +160,17 @@ class NotifyService < BaseService
|
|||
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
||||
WHERE s.id = :id
|
||||
UNION ALL
|
||||
SELECT s.id, s.in_reply_to_id, m.id, st.path || s.id, st.depth + 1
|
||||
FROM ancestors st
|
||||
JOIN statuses s ON s.id = st.in_reply_to_id
|
||||
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
||||
WHERE st.mention_id IS NULL AND NOT s.id = ANY(path) AND st.depth < :depth_limit
|
||||
SELECT s.id, s.in_reply_to_id, m.id, ancestors.path || s.id, ancestors.depth + 1
|
||||
FROM ancestors
|
||||
JOIN statuses s ON s.id = ancestors.in_reply_to_id
|
||||
/* early exit if we already have a mention matching our requirements */
|
||||
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id AND s.account_id = :recipient_id
|
||||
WHERE ancestors.mention_id IS NULL AND NOT s.id = ANY(path) AND ancestors.depth < :depth_limit
|
||||
)
|
||||
SELECT COUNT(*)
|
||||
FROM ancestors st
|
||||
JOIN statuses s ON s.id = st.id
|
||||
WHERE st.mention_id IS NOT NULL AND s.visibility = 3
|
||||
FROM ancestors
|
||||
JOIN statuses s ON s.id = ancestors.id
|
||||
WHERE ancestors.mention_id IS NOT NULL AND s.account_id = :recipient_id AND s.visibility = 3
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
|
|
@ -284,7 +284,7 @@ class PostStatusService < BaseService
|
|||
return
|
||||
end
|
||||
|
||||
media_max = @options[:poll] ? MediaAttachment::LOCAL_STATUS_ATTACHMENT_MAX_WITH_POLL : MediaAttachment::LOCAL_STATUS_ATTACHMENT_MAX
|
||||
media_max = @options[:poll] ? Status::MEDIA_ATTACHMENTS_LIMIT_WITH_POLL : Status::MEDIA_ATTACHMENTS_LIMIT
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > media_max
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ class ReportService < BaseService
|
|||
|
||||
# If the account making reports is remote, it is likely anonymized so we have to relax the requirements for attaching statuses.
|
||||
domain = @source_account.domain.to_s.downcase
|
||||
has_followers = @target_account.followers.where(Account.arel_table[:domain].lower.eq(domain)).exists?
|
||||
has_followers = @target_account.followers.with_domain(domain).exists?
|
||||
visibility = has_followers ? %i(public unlisted public_unlisted login private) : %i(public unlisted public_unlisted)
|
||||
scope = @target_account.statuses.with_discarded
|
||||
scope.merge!(scope.where(visibility: visibility).or(scope.where('EXISTS (SELECT 1 FROM mentions m JOIN accounts a ON m.account_id = a.id WHERE lower(a.domain) = ?)', domain)))
|
||||
|
|
|
@ -2,15 +2,25 @@
|
|||
|
||||
class StatusesSearchService < BaseService
|
||||
def call(query, account = nil, options = {})
|
||||
@query = query&.strip
|
||||
@account = account
|
||||
@options = options
|
||||
@limit = options[:limit].to_i
|
||||
@offset = options[:offset].to_i
|
||||
@searchability = options[:searchability]&.to_sym
|
||||
MastodonOTELTracer.in_span('StatusesSearchService#call') do |span|
|
||||
@query = query&.strip
|
||||
@account = account
|
||||
@options = options
|
||||
@limit = options[:limit].to_i
|
||||
@offset = options[:offset].to_i
|
||||
@searchability = options[:searchability]&.to_sym
|
||||
convert_deprecated_options!
|
||||
|
||||
convert_deprecated_options!
|
||||
status_search_results
|
||||
span.add_attributes(
|
||||
'search.offset' => @offset,
|
||||
'search.limit' => @limit,
|
||||
'search.backend' => Chewy.enabled? ? 'elasticsearch' : 'database'
|
||||
)
|
||||
|
||||
status_search_results.tap do |results|
|
||||
span.set_attribute('search.results.count', results.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -2,15 +2,25 @@
|
|||
|
||||
class TagSearchService < BaseService
|
||||
def call(query, options = {})
|
||||
@query = query.strip.delete_prefix('#')
|
||||
@offset = options.delete(:offset).to_i
|
||||
@limit = options.delete(:limit).to_i
|
||||
@options = options
|
||||
MastodonOTELTracer.in_span('TagSearchService#call') do |span|
|
||||
@query = query.strip.delete_prefix('#')
|
||||
@offset = options.delete(:offset).to_i
|
||||
@limit = options.delete(:limit).to_i
|
||||
@options = options
|
||||
|
||||
results = from_elasticsearch if Chewy.enabled?
|
||||
results ||= from_database
|
||||
span.add_attributes(
|
||||
'search.offset' => @offset,
|
||||
'search.limit' => @limit,
|
||||
'search.backend' => Chewy.enabled? ? 'elasticsearch' : 'database'
|
||||
)
|
||||
|
||||
results
|
||||
results = from_elasticsearch if Chewy.enabled?
|
||||
results ||= from_database
|
||||
|
||||
span.set_attribute('search.results.count', results.size)
|
||||
|
||||
results
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -152,7 +152,7 @@ class UpdateStatusService < BaseService
|
|||
def validate_media!
|
||||
return [] if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)
|
||||
|
||||
media_max = @options[:poll] ? MediaAttachment::LOCAL_STATUS_ATTACHMENT_MAX_WITH_POLL : MediaAttachment::LOCAL_STATUS_ATTACHMENT_MAX
|
||||
media_max = @options[:poll] ? Status::MEDIA_ATTACHMENTS_LIMIT_WITH_POLL : Status::MEDIA_ATTACHMENTS_LIMIT
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > media_max
|
||||
|
||||
|
|
|
@ -6,10 +6,7 @@ class UniqueUsernameValidator < ActiveModel::Validator
|
|||
def validate(account)
|
||||
return if account.username.blank?
|
||||
|
||||
normalized_username = account.username.downcase
|
||||
normalized_domain = account.domain&.downcase
|
||||
|
||||
scope = Account.where(Account.arel_table[:username].lower.eq normalized_username).where(Account.arel_table[:domain].lower.eq normalized_domain)
|
||||
scope = Account.with_username(account.username).with_domain(account.domain)
|
||||
scope = scope.where.not(id: account.id) if account.persisted?
|
||||
|
||||
account.errors.add(:username, :taken) if scope.exists?
|
||||
|
|
|
@ -22,7 +22,6 @@ class Scheduler::VacuumScheduler
|
|||
preview_cards_vacuum,
|
||||
backups_vacuum,
|
||||
access_tokens_vacuum,
|
||||
applications_vacuum,
|
||||
feeds_vacuum,
|
||||
imports_vacuum,
|
||||
list_statuses_vacuum,
|
||||
|
@ -62,10 +61,6 @@ class Scheduler::VacuumScheduler
|
|||
Vacuum::ImportsVacuum.new
|
||||
end
|
||||
|
||||
def applications_vacuum
|
||||
Vacuum::ApplicationsVacuum.new
|
||||
end
|
||||
|
||||
def ng_histories_vacuum
|
||||
Vacuum::NgHistoriesVacuum.new
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue