Browse Source

Merge branch 'main' of git.lrk.sh:comm.network/mastodon

Conflicts:
	Gemfile.lock
	app/javascript/mastodon/components/icon_button.js
	app/javascript/mastodon/locales/ru.json
	app/javascript/mastodon/locales/zh-CN.json
	app/lib/formatter.rb
	app/lib/user_settings_decorator.rb
	app/models/account.rb
	boxfile.yml
	config/locales/ast.yml
	config/locales/en.yml
	config/locales/ru.yml
	db/structure.sql
	docker-compose.yml
	package.json
	spec/rails_helper.rb
	yarn.lock
main
Lerk 1 month ago
parent
commit
ac32e97ffe
  1. 2
      .gitignore
  2. 2
      app/javascript/mastodon/components/icon_button.js
  3. 546
      app/javascript/mastodon/locales/ru.json
  4. 4
      app/javascript/packs/error.js
  5. 236
      app/javascript/packs/starfield.js
  6. 8
      app/javascript/styles/mastodon/containers.scss
  7. 48
      app/javascript/styles/night/components.scss
  8. 20
      app/javascript/styles/night/diff.scss
  9. 4
      app/javascript/styles/night/variables.scss
  10. 18
      app/lib/admin/system_check/elasticsearch_check.rb
  11. 371
      app/lib/formatter.rb
  12. 2
      app/serializers/rest/status_serializer.rb
  13. 3
      config/locales/ku.yml
  14. 1689
      config/locales/ru.yml
  15. 6
      docker-compose.yml
  16. 1
      package.json

2
.gitignore

@ -65,3 +65,5 @@ yarn-debug.log
# Ignore Docker option files
docker-compose.override.yml
*.local

2
app/javascript/mastodon/components/icon_button.js

@ -27,10 +27,10 @@ export default class IconButton extends React.PureComponent {
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
octicon: PropTypes.func,
counter: PropTypes.number,
obfuscateCount: PropTypes.bool,
href: PropTypes.string,
octicon: PropTypes.func,
};
static defaultProps = {

546
app/javascript/mastodon/locales/ru.json

@ -1,546 +0,0 @@
{
"account.account_note_header": "Заметка",
"account.add_or_remove_from_list": "Управление списками",
"account.badges.bot": "Бот",
"account.badges.group": "Группа",
"account.block": "Заблокировать @{name}",
"account.block_domain": "Заблокировать {domain}",
"account.blocked": "Заблокирован(а)",
"account.browse_more_on_origin_server": "Посмотреть в оригинальном профиле",
"account.cancel_follow_request": "Отменить запрос",
"account.direct": "Написать @{name}",
"account.disable_notifications": "Отключить уведомления от @{name}",
"account.domain_blocked": "Домен заблокирован",
"account.edit_profile": "Редактировать профиль",
"account.enable_notifications": "Включить уведомления для @{name}",
"account.endorse": "Рекомендовать в профиле",
"account.follow": "Подписаться",
"account.followers": "Подписчики",
"account.followers.empty": "На этого пользователя пока никто не подписан.",
"account.followers_counter": "{count, plural, one {{counter} подписчик} many {{counter} подписчиков} other {{counter} подписчика}}",
"account.following": "Подписки",
"account.following_counter": "{count, plural, one {{counter} подписка} many {{counter} подписок} other {{counter} подписки}}",
"account.follows.empty": "Этот пользователь пока ни на кого не подписался.",
"account.follows_you": "Подписан(а) на вас",
"account.hide_reblogs": "Скрыть продвижения от @{name}",
"account.joined": "Зарегистрирован(а) с {date}",
"account.link_verified_on": "Владение этой ссылкой было проверено {date}",
"account.locked_info": "Это закрытый аккаунт. Его владелец вручную одобряет подписчиков.",
"account.media": "Медиа",
"account.mention": "Упомянуть @{name}",
"account.moved_to": "Ищите {name} здесь:",
"account.mute": "Игнорировать @{name}",
"account.mute_notifications": "Скрыть уведомления от @{name}",
"account.muted": "Игнорируется",
"account.posts": "Посты",
"account.posts_with_replies": "Посты и ответы",
"account.report": "Пожаловаться на @{name}",
"account.requested": "Ожидает подтверждения. Нажмите для отмены запроса",
"account.share": "Поделиться профилем @{name}",
"account.show_reblogs": "Показывать продвижения от @{name}",
"account.statuses_counter": "{count, plural, one {{counter} пост} many {{counter} постов} other {{counter} поста}}",
"account.unblock": "Разблокировать @{name}",
"account.unblock_domain": "Разблокировать {domain}",
"account.unblock_short": "Разблокировать",
"account.unendorse": "Не рекомендовать в профиле",
"account.unfollow": "Отписаться",
"account.unmute": "Не игнорировать @{name}",
"account.unmute_notifications": "Показывать уведомления от @{name}",
"account.unmute_short": "Не игнорировать",
"account_note.placeholder": "Текст заметки",
"admin.dashboard.daily_retention": "Уровень удержания пользователей после регистрации, в днях",
"admin.dashboard.monthly_retention": "Уровень удержания пользователей после регистрации, в месяцах",
"admin.dashboard.retention.average": "Среднее",
"admin.dashboard.retention.cohort": "Месяц регистрации",
"admin.dashboard.retention.cohort_size": "Новые пользователи",
"alert.rate_limited.message": "Пожалуйста, повторите после {retry_time, time, medium}.",
"alert.rate_limited.title": "Ограничение количества запросов",
"alert.unexpected.message": "Произошла непредвиденная ошибка.",
"alert.unexpected.title": "Упс!",
"announcement.announcement": "Объявление",
"attachments_list.unprocessed": "(не обработан)",
"autosuggest_hashtag.per_week": "{count} / неделю",
"boost_modal.combo": "{combo}, чтобы пропустить это в следующий раз",
"bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
"bundle_column_error.retry": "Попробовать снова",
"bundle_column_error.title": "Ошибка сети",
"bundle_modal_error.close": "Закрыть",
"bundle_modal_error.message": "Что-то пошло не так при загрузке этого компонента.",
"bundle_modal_error.retry": "Попробовать снова",
"column.blocks": "Заблокированные пользователи",
"column.bookmarks": "Закладки",
"column.community": "Локальная лента",
"column.direct": "Личные сообщения",
"column.directory": "Просмотр профилей",
"column.domain_blocks": "Заблокированные домены",
"column.favourites": "Избранное",
"column.follow_requests": "Запросы на подписку",
"column.home": "Главная",
"column.lists": "Списки",
"column.mutes": "Игнорируемые пользователи",
"column.notifications": "Уведомления",
"column.pins": "Закреплённый пост",
"column.public": "Глобальная лента",
"column_back_button.label": "Назад",
"column_header.hide_settings": "Скрыть настройки",
"column_header.moveLeft_settings": "Передвинуть колонку влево",
"column_header.moveRight_settings": "Передвинуть колонку вправо",
"column_header.pin": "Закрепить",
"column_header.show_settings": "Показать настройки",
"column_header.unpin": "Открепить",
"column_subheading.settings": "Настройки",
"community.column_settings.local_only": "Только локальные",
"community.column_settings.media_only": "Только с медиафайлами",
"community.column_settings.remote_only": "Только удалённые",
"compose_form.direct_message_warning_learn_more": "Подробнее",
"compose_form.encryption_warning": "Посты в Mastodon не защищены сквозным шифрованием. Не делитесь потенциально опасной информацией.",
"compose_form.hashtag_warning": "Так как этот пост не публичный, он не отобразится в поиске по хэштегам.",
"compose_form.lock_disclaimer": "Ваша учётная запись {locked}. Любой пользователь сможет подписаться на вас и просматривать посты для подписчиков.",
"compose_form.lock_disclaimer.lock": "не закрыта",
"compose_form.placeholder": "О чём думаете?",
"compose_form.poll.add_option": "Добавить вариант",
"compose_form.poll.duration": "Продолжительность опроса",
"compose_form.poll.option_placeholder": "Вариант {number}",
"compose_form.poll.remove_option": "Убрать этот вариант",
"compose_form.poll.switch_to_multiple": "Разрешить выбор нескольких вариантов",
"compose_form.poll.switch_to_single": "Переключить в режим выбора одного ответа",
"compose_form.publish": "Запостить",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Сохранить",
"compose_form.sensitive.hide": "{count, plural, one {Отметить медифайл как деликатный} other {Отметить медифайлы как деликатные}}",
"compose_form.sensitive.marked": "Медиа{count, plural, =1 {файл отмечен} other {файлы отмечены}} как «деликатного характера»",
"compose_form.sensitive.unmarked": "Медиа{count, plural, =1 {файл не отмечен} other {файлы не отмечены}} как «деликатного характера»",
"compose_form.spoiler.marked": "Текст скрыт за предупреждением",
"compose_form.spoiler.unmarked": "Текст не скрыт",
"compose_form.spoiler_placeholder": "Текст предупреждения",
"confirmation_modal.cancel": "Отмена",
"confirmations.block.block_and_report": "Заблокировать и пожаловаться",
"confirmations.block.confirm": "Заблокировать",
"confirmations.block.message": "Вы уверены, что хотите заблокировать {name}?",
"confirmations.delete.confirm": "Удалить",
"confirmations.delete.message": "Вы уверены, что хотите удалить этот пост?",
"confirmations.delete_list.confirm": "Удалить",
"confirmations.delete_list.message": "Вы действительно хотите навсегда удалить этот список?",
"confirmations.discard_edit_media.confirm": "Отменить",
"confirmations.discard_edit_media.message": "У вас имеются несохранённые изменения превью и описания медиафайла, отменить их?",
"confirmations.domain_block.confirm": "Да, заблокировать узел",
"confirmations.domain_block.message": "Вы точно уверены, что хотите скрыть все посты с узла {domain}? В большинстве случаев пары блокировок и скрытий вполне достаточно.\n\nПри блокировке узла, вы перестанете получать уведомления оттуда, все посты будут скрыты из публичных лент, а подписчики убраны.",
"confirmations.logout.confirm": "Выйти",
"confirmations.logout.message": "Вы уверены, что хотите выйти?",
"confirmations.mute.confirm": "Игнорировать",
"confirmations.mute.explanation": "Это действие скроет посты данного пользователя и те, в которых он упоминается, но при этом он по-прежнему сможет подписаться и смотреть ваши посты.",
"confirmations.mute.message": "Вы уверены, что хотите добавить {name} в список игнорируемых?",
"confirmations.redraft.confirm": "Удалить и исправить",
"confirmations.redraft.message": "Вы уверены, что хотите отредактировать этот пост? Старый пост будет удалён, а вместе с ним пропадут отметки «В избранное», продвижения и ответы.",
"confirmations.reply.confirm": "Ответить",
"confirmations.reply.message": "При ответе, текст набираемого поста будет очищен. Продолжить?",
"confirmations.unfollow.confirm": "Отписаться",
"confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
"conversation.delete": "Удалить беседу",
"conversation.mark_as_read": "Отметить как прочитанное",
"conversation.open": "Просмотр беседы",
"conversation.with": "С {names}",
"directory.federated": "Со всей федерации",
"directory.local": "Только с {domain}",
"directory.new_arrivals": "Новички",
"directory.recently_active": "Недавно активные",
"embed.instructions": "Встройте этот пост на свой сайт, скопировав следующий код:",
"embed.preview": "Так это будет выглядеть:",
"emoji_button.activity": "Занятия",
"emoji_button.custom": "С этого узла",
"emoji_button.flags": "Флаги",
"emoji_button.food": "Еда и напитки",
"emoji_button.label": "Вставить эмодзи",
"emoji_button.nature": "Природа",
"emoji_button.not_found": "Нет эмодзи!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Предметы",
"emoji_button.people": "Люди",
"emoji_button.recent": "Последние",
"emoji_button.search": "Найти...",
"emoji_button.search_results": "Результаты поиска",
"emoji_button.symbols": "Символы",
"emoji_button.travel": "Путешествия и места",
"empty_column.account_suspended": "Учетная запись заблокирована",
"empty_column.account_timeline": "Здесь нет постов!",
"empty_column.account_unavailable": "Профиль недоступен",
"empty_column.blocks": "Вы ещё никого не заблокировали.",
"empty_column.bookmarked_statuses": "У вас пока нет постов в закладках. Как добавите один, он отобразится здесь.",
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
"empty_column.direct": "У вас пока нет личных сообщений. Как только вы отправите или получите одно, оно появится здесь.",
"empty_column.domain_blocks": "Скрытых доменов пока нет.",
"empty_column.explore_statuses": "Нет актуального. Проверьте позже!",
"empty_column.favourited_statuses": "Вы не добавили ни один пост в «Избранное». Как только вы это сделаете, он появится здесь.",
"empty_column.favourites": "Никто ещё не добавил этот пост в «Избранное». Как только кто-то это сделает, это отобразится здесь.",
"empty_column.follow_recommendations": "Похоже, у нас нет предложений для вас. Вы можете попробовать поискать людей, которых уже знаете, или изучить актуальные хэштеги.",
"empty_column.follow_requests": "Вам ещё не приходили запросы на подписку. Все новые запросы будут показаны здесь.",
"empty_column.hashtag": "С этим хэштегом пока ещё ничего не постили.",
"empty_column.home": "Ваша лента совсем пуста! Подпишитесь на других, чтобы заполнить её. {suggestions}",
"empty_column.home.suggestions": "Посмотреть некоторые предложения",
"empty_column.list": "В этом списке пока ничего нет.",
"empty_column.lists": "У вас ещё нет списков. Созданные вами списки будут показаны здесь.",
"empty_column.mutes": "Вы ещё никого не добавляли в список игнорируемых.",
"empty_column.notifications": "У вас пока нет уведомлений. Взаимодействуйте с другими, чтобы завести разговор.",
"empty_column.public": "Здесь совсем пусто. Опубликуйте что-нибудь или подпишитесь на пользователей с других сообществ, чтобы заполнить ленту",
"error.unexpected_crash.explanation": "Из-за несовместимого браузера или ошибки в нашем коде, эта страница не может быть корректно отображена.",
"error.unexpected_crash.explanation_addons": "Эта страница не может быть корректно отображена. Скорее всего, эта ошибка вызвана расширением браузера или инструментом автоматического перевода.",
"error.unexpected_crash.next_steps": "Попробуйте обновить страницу. Если проблема не исчезает, используйте Mastodon из-под другого браузера или приложения.",
"error.unexpected_crash.next_steps_addons": "Попробуйте их отключить и перезагрузить страницу. Если это не поможет, вы по-прежнему сможете войти в Mastodon через другой браузер или приложение.",
"errors.unexpected_crash.copy_stacktrace": "Скопировать диагностическую информацию",
"errors.unexpected_crash.report_issue": "Сообщить о проблеме",
"explore.search_results": "Результаты поиска",
"explore.suggested_follows": "Для вас",
"explore.title": "Обзор",
"explore.trending_links": "Новости",
"explore.trending_statuses": "Посты",
"explore.trending_tags": "Хэштеги",
"follow_recommendations.done": "Готово",
"follow_recommendations.heading": "Подпишитесь на людей, чьи посты вы бы хотели видеть. Вот несколько предложений.",
"follow_recommendations.lead": "Посты от людей, на которых вы подписаны, будут отображаться в вашей домашней ленте в хронологическом порядке. Не бойтесь ошибиться — вы так же легко сможете отписаться от них в любое время!",
"follow_request.authorize": "Авторизовать",
"follow_request.reject": "Отказать",
"follow_requests.unlocked_explanation": "Этот запрос отправлен с учётной записи, для которой администрация {domain} включила ручную проверку подписок.",
"generic.saved": "Сохранено",
"getting_started.developers": "Разработчикам",
"getting_started.directory": "Каталог профилей",
"getting_started.documentation": "Документация",
"getting_started.heading": "Начать",
"getting_started.invite": "Пригласить людей",
"getting_started.open_source_notice": "Mastodon — сервис с открытым исходным кодом. Вы можете внести вклад или сообщить о проблемах на GitHub: {github}.",
"getting_started.security": "Настройки учётной записи",
"getting_started.terms": "Условия использования",
"hashtag.column_header.tag_mode.all": "и {additional}",
"hashtag.column_header.tag_mode.any": "или {additional}",
"hashtag.column_header.tag_mode.none": "без {additional}",
"hashtag.column_settings.select.no_options_message": "Предложений не найдено",
"hashtag.column_settings.select.placeholder": "Введите хэштеги…",
"hashtag.column_settings.tag_mode.all": "Все из списка",
"hashtag.column_settings.tag_mode.any": "Любой из списка",
"hashtag.column_settings.tag_mode.none": "Ни один из списка",
"hashtag.column_settings.tag_toggle": "Включить дополнительные теги для этой колонки",
"home.column_settings.basic": "Основные",
"home.column_settings.show_reblogs": "Показывать продвижения",
"home.column_settings.show_replies": "Показывать ответы",
"home.hide_announcements": "Скрыть объявления",
"home.show_announcements": "Показать объявления",
"intervals.full.days": "{number, plural, one {# день} few {# дня} other {# дней}}",
"intervals.full.hours": "{number, plural, one {# час} few {# часа} other {# часов}}",
"intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} other {# минут}}",
"keyboard_shortcuts.back": "перейти назад",
"keyboard_shortcuts.blocked": "чтобы открыть список заблокированных",
"keyboard_shortcuts.boost": "продвинуть пост",
"keyboard_shortcuts.column": "фокус на одном из столбцов",
"keyboard_shortcuts.compose": "фокус на поле ввода",
"keyboard_shortcuts.description": "Описание",
"keyboard_shortcuts.direct": "чтобы открыть колонку с личными сообщениями",
"keyboard_shortcuts.down": "вниз по списку",
"keyboard_shortcuts.enter": "открыть пост",
"keyboard_shortcuts.favourite": "в избранное",
"keyboard_shortcuts.favourites": "открыть «Избранное»",
"keyboard_shortcuts.federated": "перейти к глобальной ленте",
"keyboard_shortcuts.heading": "Сочетания клавиш",
"keyboard_shortcuts.home": "перейти к домашней ленте",
"keyboard_shortcuts.hotkey": "Гор. клавиша",
"keyboard_shortcuts.legend": "показать это окно",
"keyboard_shortcuts.local": "перейти к локальной ленте",
"keyboard_shortcuts.mention": "упомянуть автора поста",
"keyboard_shortcuts.muted": "открыть список игнорируемых",
"keyboard_shortcuts.my_profile": "перейти к своему профилю",
"keyboard_shortcuts.notifications": "перейти к уведомлениям",
"keyboard_shortcuts.open_media": "открыть вложение",
"keyboard_shortcuts.pinned": "перейти к закреплённым постам",
"keyboard_shortcuts.profile": "перейти к профилю автора",
"keyboard_shortcuts.reply": "ответить",
"keyboard_shortcuts.requests": "перейти к запросам на подписку",
"keyboard_shortcuts.search": "перейти к поиску",
"keyboard_shortcuts.spoilers": "показать/скрыть поле предупреждения о содержании",
"keyboard_shortcuts.start": "Перейти к разделу \"Начать\"",
"keyboard_shortcuts.toggle_hidden": "показать/скрыть текст за предупреждением",
"keyboard_shortcuts.toggle_sensitivity": "Показать/скрыть медиафайлы",
"keyboard_shortcuts.toot": "начать писать новый пост",
"keyboard_shortcuts.unfocus": "убрать фокус с поля ввода/поиска",
"keyboard_shortcuts.up": "вверх по списку",
"lightbox.close": "Закрыть",
"lightbox.compress": "Сжать окно просмотра изображений",
"lightbox.expand": "Развернуть окно просмотра изображений",
"lightbox.next": "Далее",
"lightbox.previous": "Назад",
"limited_account_hint.action": "Show profile anyway",
"limited_account_hint.title": "This profile has been hidden by the moderators of your server.",
"lists.account.add": "Добавить в список",
"lists.account.remove": "Убрать из списка",
"lists.delete": "Удалить список",
"lists.edit": "Изменить список",
"lists.edit.submit": "Изменить название",
"lists.new.create": "Создать список",
"lists.new.title_placeholder": "Название для нового списка",
"lists.replies_policy.followed": "Любой подписанный пользователь",
"lists.replies_policy.list": "Пользователи в списке",
"lists.replies_policy.none": "Никого",
"lists.replies_policy.title": "Показать ответы только:",
"lists.search": "Искать среди подписок",
"lists.subheading": "Ваши списки",
"load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}",
"loading_indicator.label": "Загрузка...",
"media_gallery.toggle_visible": "Показать/скрыть {number, plural, =1 {изображение} other {изображения}}",
"missing_indicator.label": "Не найдено",
"missing_indicator.sublabel": "Запрашиваемый ресурс не найден",
"mute_modal.duration": "Продолжительность",
"mute_modal.hide_notifications": "Скрыть уведомления от этого пользователя?",
"mute_modal.indefinite": "Не определена",
"navigation_bar.apps": "Мобильные приложения",
"navigation_bar.blocks": "Список блокировки",
"navigation_bar.bookmarks": "Закладки",
"navigation_bar.community_timeline": "Локальная лента",
"navigation_bar.compose": "Создать новый пост",
"navigation_bar.direct": "Личные сообщения",
"navigation_bar.discover": "Изучайте",
"navigation_bar.domain_blocks": "Скрытые домены",
"navigation_bar.edit_profile": "Изменить профиль",
"navigation_bar.explore": "Обзор",
"navigation_bar.favourites": "Избранное",
"navigation_bar.filters": "Игнорируемые слова",
"navigation_bar.follow_requests": "Запросы на подписку",
"navigation_bar.follows_and_followers": "Подписки и подписчики",
"navigation_bar.info": "Об узле",
"navigation_bar.keyboard_shortcuts": "Сочетания клавиш",
"navigation_bar.lists": "Списки",
"navigation_bar.logout": "Выйти",
"navigation_bar.mutes": "Список игнорируемых пользователей",
"navigation_bar.personal": "Личное",
"navigation_bar.pins": "Закреплённые посты",
"navigation_bar.preferences": "Настройки",
"navigation_bar.public_timeline": "Глобальная лента",
"navigation_bar.security": "Безопасность",
"notification.admin.sign_up": "{name} зарегистрирован",
"notification.favourite": "{name} добавил(а) ваш пост в избранное",
"notification.follow": "{name} подписался (-лась) на вас",
"notification.follow_request": "{name} отправил запрос на подписку",
"notification.mention": "{name} упомянул(а) вас",
"notification.own_poll": "Ваш опрос закончился",
"notification.poll": "Опрос, в котором вы приняли участие, завершился",
"notification.reblog": "{name} продвинул(а) ваш пост",
"notification.status": "{name} только что запостил",
"notification.update": "{name} изменил(а) пост",
"notifications.clear": "Очистить уведомления",
"notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?",
"notifications.column_settings.admin.sign_up": "Новые регистрации:",
"notifications.column_settings.alert": "Уведомления на рабочем столе",
"notifications.column_settings.favourite": "Ваш пост добавили в «избранное»:",
"notifications.column_settings.filter_bar.advanced": "Отображать все категории",
"notifications.column_settings.filter_bar.category": "Панель сортировки",
"notifications.column_settings.filter_bar.show_bar": "Отображать панель сортировки",
"notifications.column_settings.follow": "У вас новый подписчик:",
"notifications.column_settings.follow_request": "Новые запросы на подписку:",
"notifications.column_settings.mention": "Вас упомянули в посте:",
"notifications.column_settings.poll": "Опрос, в котором вы приняли участие, завершился:",
"notifications.column_settings.push": "Пуш-уведомления",
"notifications.column_settings.reblog": "Ваш пост продвинули:",
"notifications.column_settings.show": "Отображать в списке",
"notifications.column_settings.sound": "Проигрывать звук",
"notifications.column_settings.status": "Новые посты:",
"notifications.column_settings.unread_notifications.category": "Непрочитанные уведомления",
"notifications.column_settings.unread_notifications.highlight": "Выделять непрочитанные уведомления",
"notifications.column_settings.update": "Правки:",
"notifications.filter.all": "Все",
"notifications.filter.boosts": "Продвижения",
"notifications.filter.favourites": "Отметки «избранного»",
"notifications.filter.follows": "Подписки",
"notifications.filter.mentions": "Упоминания",
"notifications.filter.polls": "Результаты опросов",
"notifications.filter.statuses": "Обновления от людей, на которых вы подписаны",
"notifications.grant_permission": "Предоставить разрешение.",
"notifications.group": "{count} уведомл.",
"notifications.mark_as_read": "Отмечать все уведомления прочитанными",
"notifications.permission_denied": "Уведомления на рабочем столе недоступны, так как вы запретили их отправку в браузере. Проверьте настройки для сайта, чтобы включить их обратно.",
"notifications.permission_denied_alert": "Уведомления на рабочем столе недоступны, так как вы ранее отклонили запрос на их отправку.",
"notifications.permission_required": "Чтобы включить уведомления на рабочем столе, необходимо разрешить их в браузере.",
"notifications_permission_banner.enable": "Включить уведомления",
"notifications_permission_banner.how_to_control": "Получайте уведомления даже когда Mastodon закрыт, включив уведомления на рабочем столе. А чтобы лишний шум не отвлекал, вы можете настроить какие уведомления вы хотите получать, нажав на кнопку {icon} выше.",
"notifications_permission_banner.title": "Будьте в курсе происходящего",
"picture_in_picture.restore": "Вернуть обратно",
"poll.closed": "Завершён",
"poll.refresh": "Обновить",
"poll.total_people": "{count, plural, one {# человек} few {# человека} many {# человек} other {# человек}}",
"poll.total_votes": "{count, plural, one {# голос} few {# голоса} many {# голосов} other {# голосов}}",
"poll.vote": "Голосовать",
"poll.voted": "Вы проголосовали за этот вариант",
"poll.votes": "{votes, plural, one {# голос} many {# голосов} other {# голоса}}",
"poll_button.add_poll": "Добавить опрос",
"poll_button.remove_poll": "Удалить опрос",
"privacy.change": "Изменить видимость поста",
"privacy.direct.long": "Показать только упомянутым",
"privacy.direct.short": "Только упомянутые",
"privacy.private.long": "Показать только подписчикам",
"privacy.private.short": "Для подписчиков",
"privacy.public.long": "Виден всем",
"privacy.public.short": "Публичный",
"privacy.unlisted.long": "Виден всем, но не через функции обзора",
"privacy.unlisted.short": "Скрытый",
"refresh": "Обновить",
"regeneration_indicator.label": "Загрузка…",
"regeneration_indicator.sublabel": "Один момент, мы подготавливаем вашу ленту!",
"relative_time.days": "{number} д",
"relative_time.full.days": "{number, plural, one {# день} many {# дней} other {# дня}} назад",
"relative_time.full.hours": "{number, plural, one {# час} many {# часов} other {# часа}} назад",
"relative_time.full.just_now": "только что",
"relative_time.full.minutes": "{number, plural, one {# минуту} many {# минут} other {# минуты}} назад",
"relative_time.full.seconds": "{number, plural, one {# секунду} many {# секунд} other {# секунды}} назад",
"relative_time.hours": "{number} ч",
"relative_time.just_now": "только что",
"relative_time.minutes": "{number} мин",
"relative_time.seconds": "{number} с",
"relative_time.today": "сегодня",
"reply_indicator.cancel": "Отмена",
"report.block": "Заблокировать",
"report.block_explanation": "В перестаните видеть посты этого пользователя, а он(а) больше не сможет подписаться на вас и читать ваши посты. Он(а) сможет понять что вы заблокировали его/её.",
"report.categories.other": "Другое",
"report.categories.spam": "Спам",
"report.categories.violation": "Содержимое нарушает одно или несколько правил узла",
"report.category.subtitle": "Выберите наиболее подходящее",
"report.category.title": "Расскажите нам, что не так с {type}",
"report.category.title_account": "этим профилем",
"report.category.title_status": "этим постом",
"report.close": "Готово",
"report.comment.title": "Есть ли что-нибудь ещё, что нам стоит знать?",
"report.forward": "Переслать на {target}",
"report.forward_hint": "Эта учётная запись расположена на другом узле. Отправить туда анонимную копию вашей жалобы?",
"report.mute": "Игнорировать",
"report.mute_explanation": "Вы не будете видеть их посты. Они по-прежнему могут подписываться на вас и видеть ваши посты, но не будут знать, что они в списке игнорируемых.",
"report.next": "Далее",
"report.placeholder": "Дополнительные комментарии",
"report.reasons.dislike": "Мне не нравится",
"report.reasons.dislike_description": "Не хотел(а) бы видеть такой контент",
"report.reasons.other": "Другое",
"report.reasons.other_description": "Проблема не попадает ни под одну из категорий",
"report.reasons.spam": "Это спам",
"report.reasons.spam_description": "Вредоносные ссылки, фальшивое взаимодействие или повторяющиеся ответы",
"report.reasons.violation": "Нарушаются правила сервера",
"report.reasons.violation_description": "Вы знаете, что подобное нарушает определенные правила",
"report.rules.subtitle": "Выберите все подходящие варианты",
"report.rules.title": "Какие правила нарушены?",
"report.statuses.subtitle": "Выберите все подходящие варианты",
"report.statuses.title": "Выберите посты, которые относятся к вашей жалобе.",
"report.submit": "Отправить",
"report.target": "Жалоба на {target}",
"report.thanks.take_action": "Вот несколько опций управления тем, что вы видите в Mastodon:",
"report.thanks.take_action_actionable": "Пока мы рассматриваем его, вот действия, которые вы можете предпринять лично против @{name}:",
"report.thanks.title": "Не хотите видеть это?",
"report.thanks.title_actionable": "Спасибо за обращение, мы его рассмотрим.",
"report.unfollow": "Отписаться от @{name}",
"report.unfollow_explanation": "Вы подписаны на этого пользователя. Чтобы не видеть его/её посты в своей домашней ленте, отпишитесь от него/неё.",
"search.placeholder": "Поиск",
"search_popout.search_format": "Продвинутый формат поиска",
"search_popout.tips.full_text": "Поиск по простому тексту отобразит посты, которые вы написали, добавили в избранное, продвинули или в которых были упомянуты, а также подходящие имена пользователей и хэштеги.",
"search_popout.tips.hashtag": "хэштег",
"search_popout.tips.status": "пост",
"search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги",
"search_popout.tips.user": "пользователь",
"search_results.accounts": "Люди",
"search_results.all": "Все",
"search_results.hashtags": "Хэштеги",
"search_results.nothing_found": "Ничего не найдено по этому запросу",
"search_results.statuses": "Посты",
"search_results.statuses_fts_disabled": "Поиск постов по их содержанию не поддерживается данным сервером Mastodon.",
"search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
"status.admin_account": "Открыть интерфейс модератора для @{name}",
"status.admin_status": "Открыть этот пост в интерфейсе модератора",
"status.block": "Заблокировать @{name}",
"status.bookmark": "Сохранить в закладки",
"status.cancel_reblog_private": "Не продвигать",
"status.cannot_reblog": "Этот пост не может быть продвинут",
"status.copy": "Скопировать ссылку на пост",
"status.delete": "Удалить",
"status.detailed_status": "Подробный просмотр обсуждения",
"status.direct": "Написать @{name}",
"status.edit": "Изменить",
"status.edited": "Последнее изменение: {date}",
"status.edited_x_times": "{count, plural, one {{count} изменение} many {{count} изменений} other {{count} изменения}}",
"status.embed": "Встроить на свой сайт",
"status.favourite": "В избранное",
"status.filtered": "Отфильтровано",
"status.history.created": "{name} создал {date}",
"status.history.edited": "{name} отредактировал {date}",
"status.load_more": "Загрузить остальное",
"status.media_hidden": "Файл скрыт",
"status.mention": "Упомянуть @{name}",
"status.more": "Ещё",
"status.mute": "Игнорировать @{name}",
"status.mute_conversation": "Игнорировать обсуждение",
"status.open": "Открыть пост",
"status.pin": "Закрепить в профиле",
"status.pinned": "Закреплённый пост",
"status.read_more": "Ещё",
"status.reblog": "Продвинуть",
"status.reblog_private": "Продвинуть для своей аудитории",
"status.reblogged_by": "{name} продвинул(а)",
"status.reblogs.empty": "Никто ещё не продвинул этот пост. Как только кто-то это сделает, они появятся здесь.",
"status.redraft": "Удалить и исправить",
"status.remove_bookmark": "Убрать из закладок",
"status.reply": "Ответить",
"status.replyAll": "Ответить всем",
"status.report": "Пожаловаться",
"status.sensitive_warning": "Содержимое «деликатного характера»",
"status.share": "Поделиться",
"status.show_less": "Свернуть",
"status.show_less_all": "Свернуть все спойлеры в ветке",
"status.show_more": "Развернуть",
"status.show_more_all": "Развернуть все спойлеры в ветке",
"status.show_thread": "Показать обсуждение",
"status.uncached_media_warning": "Невозможно отобразить файл",
"status.unmute_conversation": "Не игнорировать обсуждение",
"status.unpin": "Открепить от профиля",
"suggestions.dismiss": "Удалить предложение",
"suggestions.header": "Вам может быть интересно…",
"tabs_bar.federated_timeline": "Глобальная",
"tabs_bar.home": "Главная",
"tabs_bar.local_timeline": "Локальная",
"tabs_bar.notifications": "Уведомления",
"tabs_bar.search": "Поиск",
"time_remaining.days": "{number, plural, one {остался # день} few {осталось # дня} many {осталось # дней} other {осталось # дней}}",
"time_remaining.hours": "{number, plural, one {остался # час} few {осталось # часа} many {осталось # часов} other {осталось # часов}}",
"time_remaining.minutes": "{number, plural, one {осталась # минута} few {осталось # минуты} many {осталось # минут} other {осталось # минут}}",
"time_remaining.moments": "остались считанные мгновения",
"time_remaining.seconds": "{number, plural, one {# секунда} many {# секунд} other {# секунды}}",
"timeline_hint.remote_resource_not_displayed": "Мы не отображаем {resource} с других серверов.",
"timeline_hint.resources.followers": "подписчиков",
"timeline_hint.resources.follows": "подписки",
"timeline_hint.resources.statuses": "прошлые посты",
"trends.counter_by_accounts": "{count, plural, one {{counter} человек обсуждает} few {{counter} человека обсуждают} many {{counter} человек обсуждают} other {{counter} человека обсуждает}}",
"trends.trending_now": "Самое актуальное",
"ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.",
"units.short.billion": "{count} млрд",
"units.short.million": "{count} млн",
"units.short.thousand": "{count} тыс.",
"upload_area.title": "Перетащите сюда, чтобы загрузить",
"upload_button.label": "Прикрепить фото, видео или аудио",
"upload_error.limit": "Достигнут лимит загруженных файлов.",
"upload_error.poll": "К опросам нельзя прикреплять файлы.",
"upload_form.audio_description": "Опишите аудиофайл для людей с нарушением слуха",
"upload_form.description": "Добавьте описание для людей с нарушениями зрения:",
"upload_form.description_missing": "Описание не добавлено",
"upload_form.edit": "Изменить",
"upload_form.thumbnail": "Изменить обложку",
"upload_form.undo": "Отменить",
"upload_form.video_description": "Опишите видео для людей с нарушением слуха или зрения",
"upload_modal.analyzing_picture": "Обработка изображения…",
"upload_modal.apply": "Применить",
"upload_modal.applying": "Применение…",
"upload_modal.choose_image": "Выбрать изображение",
"upload_modal.description_placeholder": "На дворе трава, на траве дрова",
"upload_modal.detect_text": "Найти текст на картинке",
"upload_modal.edit_media": "Изменить файл",
"upload_modal.hint": "Нажмите и перетащите круг в предпросмотре в точку фокуса, которая всегда будет видна на эскизах.",
"upload_modal.preparing_ocr": "Подготовка распознования…",
"upload_modal.preview_label": "Предпросмотр ({ratio})",
"upload_progress.label": "Загрузка...",
"video.close": "Закрыть видео",
"video.download": "Загрузить файл",
"video.exit_fullscreen": "Покинуть полноэкранный режим",
"video.expand": "Развернуть видео",
"video.fullscreen": "Полноэкранный режим",
"video.hide": "Скрыть видео",
"video.mute": "Выключить звук",
"video.pause": "Пауза",
"video.play": "Пуск",
"video.unmute": "Включить звук"
}

4
app/javascript/packs/error.js

@ -36,7 +36,7 @@ ready(() => {
'̨', '̴', '̵', '̶',
'͜', '͝', '͞',
'͟', '͠', '͢', '̸',
'̷', '͡', ' ҉',
'̷', '͡'
],
};
@ -107,5 +107,5 @@ ready(() => {
return result;
};
document.getElementById('message').innerHTML = zalgo.heComes(document.getElementById('message').innerText, { size: 'default' });
document.getElementById('message').innerHTML = zalgo.heComes(document.getElementById('message').innerText, { size: 'maxi' });
});

236
app/javascript/packs/starfield.js

@ -1,233 +1,7 @@
/*!
The MIT License (MIT)
Copyright (c) 2015 popAD, LLC dba Rocket Wagon Labs <lukel99@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
export const COORDINATE_LENGTH = 5000;
import {runStarfield} from "starfield-webgl";
//CLASSES
/**
* The star object we're going to create
* Star's coordinate system is 0 through COORDINATE_LENGTH, and then mapped onto the coordinate system of our canvas
* @param {number} x
* @param {number} y
* @param {number} size
* @param {string} color - color string
* @return {Star} a star object
*/
export const Star = function (x, y, size, color) {
this.x = x;
this.y = y;
this.size = size;
this.color = color;
};
/**
* Convert from star X/Y (0-COORDINATE_LENGTH) to canvas X/Y
* @param {number} canvasWidth - the canvas width in pixels
* @param {number} canvasHeight - the canvas height in pixels
* @return {Object} an object containing the coordinates on the canvas
*/
Star.prototype.mapXYToCanvasCoordinates = function (canvasWidth, canvasHeight) {
const canvasX = Math.round((this.x / COORDINATE_LENGTH) * canvasWidth);
const canvasY = Math.round((this.y / COORDINATE_LENGTH) * canvasHeight);
return {
x: canvasX,
y: canvasY,
};
};
export const StarFactory = {
/**
* Generates all random values to create a random star
* @return {Star} a star with random X/Y, size and color
*/
getRandomStar: function () {
const x = Math.floor(Math.random() * (COORDINATE_LENGTH + 1));
const y = Math.floor(Math.random() * (COORDINATE_LENGTH + 1));
const size = this._getWeightedRandomSize();
const color = this._getWeightedRandomColor();
const tintedColor = this._applyRandomShade(color);
return new Star(x, y, size, this._getRGBColorString(tintedColor));
},
_getWeightedRandomSize: function () {
const list = [1, 1.5, 2];
const weight = [0.8, 0.15, 0.05];
return this._getWeightedRandom(list, weight);
},
_getWeightedRandomColor: function () {
const list = [
{ 'r': 255, 'g': 189, 'b': 111 },
{ 'r': 255, 'g': 221, 'b': 180 },
{ 'r': 255, 'g': 244, 'b': 232 },
{ 'r': 251, 'g': 248, 'b': 255 },
{ 'r': 202, 'g': 216, 'b': 255 },
{ 'r': 170, 'g': 191, 'b': 255 },
{ 'r': 155, 'g': 176, 'b': 255 },
];
const weight = [0.05, 0.05, 0.05, 0.7, 0.05, 0.05, 0.05];
return this._getWeightedRandom(list, weight);
},
_getRandomShade: function () {
const list = [0.4, 0.6, 1];
const weight = [0.5, 0.3, 0.2];
return this._getWeightedRandom(list, weight);
},
_applyRandomShade: function (color) {
const shade = this._getRandomShade();
if (shade !== 1) { // skip processing full brightness stars
color.r = Math.floor(color.r * shade);
color.g = Math.floor(color.g * shade);
color.b = Math.floor(color.b * shade);
}
return color;
},
_getRGBColorString: function (color) {
return 'rgb(' + color.r + ',' + color.g + ',' + color.b + ')';
},
// http://codetheory.in/weighted-biased-random-number-generation-with-javascript-based-on-probability/
_getWeightedRandom: function (list, weight) {
const rand = function (min, max) {
return Math.random() * (max - min) + min;
};
const total_weight = weight.reduce(function (prev, cur) {
return prev + cur;
});
const random_num = rand(0, total_weight);
let weight_sum = 0;
for (let i = 0; i < list.length; i++) {
weight_sum += weight[i];
weight_sum = +weight_sum.toFixed(2);
if (random_num <= weight_sum) {
return list[i];
}
}
return list[rand(0, list.length)];
},
};
(() => {
const Starfield = [];
Window.starfield = function (options, elem) {
const settings = {
starDensity: 1.0,
mouseScale: 1.0,
seedMovement: true, ...options,
};
let canvas = document.createElement('canvas');
canvas.id = 'starfield';
const styleProps = { position: 'fixed', left: 0, top: 0, width: '100%', height: '100%', 'z-index': -10 };
const attrProps = { width: elem.clientWidth, height: elem.clientHeight };
for (let prop in styleProps) {
canvas.style[prop] = styleProps[prop];
}
for (let prop in attrProps) {
canvas.setAttribute(prop, attrProps[prop]);
}
elem.appendChild(canvas);
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const totalPixels = width * height;
const starRatio = 0.002 * settings.starDensity;
const numStars = Math.floor(totalPixels * starRatio);
const deltaX = 0.12;
const deltaY = 0.04;
for (let i = 0; i < numStars; i++) {
Starfield.push(StarFactory.getRandomStar());
}
// ANIMATION HANDLER
const recalcMovement = function () {
Starfield.forEach((star) => {
let newX = star.x - deltaX;
let newY = star.y - deltaY;
if (newX < 0) {
newX += COORDINATE_LENGTH;
}
if (newY < 0) {
newY += COORDINATE_LENGTH;
}
if (newX > COORDINATE_LENGTH) {
newX -= COORDINATE_LENGTH;
}
if (newY > COORDINATE_LENGTH) {
newY -= COORDINATE_LENGTH;
}
star.x = newX;
star.y = newY;
});
};
const draw = function () {
//get raw DOM element
const canvas = document.getElementById('starfield');
const width = canvas.clientWidth;
const height = canvas.clientHeight;
canvas.setAttribute('width', width.toString());
canvas.setAttribute('height', height.toString());
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
// clear canvas
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height);
// iterate stars and draw them
Starfield.forEach((star) => {
const coords = star.mapXYToCanvasCoordinates(width, height);
ctx.fillStyle = star.color;
ctx.fillRect(coords.x, coords.y, star.size, star.size);
});
}
};
(function animloop() {
requestAnimationFrame(animloop);
recalcMovement();
draw();
})();
return this;
};
document.addEventListener('DOMContentLoaded', function() {
Window.starfield({ starDensity: 2.4 }, document.querySelector('body'));
document.addEventListener('DOMContentLoaded', () => {
runStarfield({
starDensity: 1.4
});
})();
});

8
app/javascript/styles/mastodon/containers.scss

@ -420,6 +420,14 @@
margin-bottom: 10px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&__tabs {
&__name {
.roles {
margin-top: 0.6rem;
}
}
}
&.inactive {
opacity: 0.5;

48
app/javascript/styles/night/components.scss

@ -1,3 +1,5 @@
@import "variables";
.app-body {
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
@ -383,7 +385,7 @@
.compose-form__warning {
color: $inverted-text-color;
margin-bottom: 10px;
background: $ui-primary-color;
background: $ui-base-color;
box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
padding: 8px 10px;
border-radius: 4px;
@ -598,17 +600,29 @@
min-width: 40%;
margin: 5px;
&__warning {
.icon-button {
color: $darker-text-color;
&:hover,
&:focus,
&:active {
color: lighten($darker-text-color, 20%);
}
}
}
&__actions {
background: linear-gradient(180deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
background: linear-gradient(180deg, rgba($base-shadow-color, 0.6) 0, rgba($base-shadow-color, 0.21) 80%, transparent);
display: flex;
align-items: flex-start;
justify-content: space-between;
opacity: 0;
opacity: 1;
transition: opacity .1s ease;
.icon-button {
flex: 0 1 auto;
color: $secondary-text-color;
color: $darker-text-color;
font-size: 14px;
font-weight: 500;
padding: 10px;
@ -617,13 +631,9 @@
&:hover,
&:focus,
&:active {
color: lighten($secondary-text-color, 7%);
color: lighten($darker-text-color, 20%);
}
}
&.active {
opacity: 1;
}
}
&-description {
@ -769,7 +779,7 @@
.reply-indicator {
border-radius: 4px;
margin-bottom: 10px;
background: $ui-primary-color;
background: $ui-base-color;
padding: 10px;
min-height: 23px;
overflow-y: auto;
@ -782,9 +792,13 @@
}
.reply-indicator__cancel {
color: $primary-text-color;
color: $darker-text-color;
float: right;
line-height: 24px;
button.icon-button {
color: $darker-text-color;
}
}
.reply-indicator__display-name {
@ -1316,7 +1330,7 @@
font-size: 14px;
a {
color: $lighter-text-color;
color: $darker-text-color;
}
}
@ -1788,6 +1802,8 @@ a.account__display-name {
}
.display-name__account {
margin-top: 0;
color: $dark-text-color;
font-size: 14px;
}
@ -1910,6 +1926,10 @@ a.account__display-name {
.navigation-bar__profile-edit {
color: inherit;
text-decoration: none;
span {
color: $dark-text-color;
}
}
.dropdown {
@ -5442,7 +5462,7 @@ a.status-card.compact:hover {
a {
text-decoration: none;
color: $dark-text-color;
color: $darker-text-color;
font-weight: 500;
&:hover {
@ -5461,7 +5481,7 @@ a.status-card.compact:hover {
}
.fa, .octicon {
color: $dark-text-color;
color: $darker-text-color;
}
}
}

20
app/javascript/styles/night/diff.scss

@ -1,3 +1,5 @@
@import "variables";
#upload-modal__description {
background: transparent;
border: 1px solid $base-border-color;
@ -40,3 +42,21 @@ select.select {
min-width: 130px !important;
height: auto !important;
}
.public-layout {
.content {
.public-account-header {
&__tabs {
&__name {
h1 {
color: white;
}
small {
color: $darker-text-color;
}
}
}
}
}
}

4
app/javascript/styles/night/variables.scss

@ -24,7 +24,7 @@ $error-value-color: $error-red !default;
// Tell UI to use selected colors
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: #808080 !default; // Lighter darkest
$ui-base-lighter-color: #656565 !default; // Lighter darkest
$ui-base-even-lighter-color: #d4d4d4 !default; // Even lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
@ -32,7 +32,7 @@ $ui-highlight-color: $classic-highlight-color !default;
// Variables for texts
$primary-text-color: $ui-base-even-lighter-color !default;
$darker-text-color: $ui-primary-color !default;
$darker-text-color: #a2a2a2 !default;
$dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: $ui-highlight-color !default;

18
app/lib/admin/system_check/elasticsearch_check.rb

@ -2,27 +2,17 @@
class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
def pass?
return true unless Chewy.enabled?
running_version.present? && compatible_version?
true
end
def message
if running_version.present?
Admin::SystemCheck::Message.new(:elasticsearch_version_check, I18n.t('admin.system_checks.elasticsearch_version_check.version_comparison', running_version: running_version, required_version: required_version))
else
Admin::SystemCheck::Message.new(:elasticsearch_running_check)
end
Admin::SystemCheck::Message.new(:elasticsearch_version_check, 'This fork is using Postgres FullText Search, Elasticsearch is not needed!')
end
private
def running_version
@running_version ||= begin
Chewy.client.info['version']['number']
rescue Faraday::ConnectionFailed
nil
end
nil
end
def required_version
@ -30,7 +20,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
end
def compatible_version?
Gem::Version.new(running_version) >= Gem::Version.new(required_version)
true
end
def missing_queues

371
app/lib/formatter.rb

@ -0,0 +1,371 @@
# frozen_string_literal: true
require 'singleton'
class HTMLRenderer < Redcarpet::Render::HTML
def block_code(code, _language)
"<pre><code>#{encode(code).gsub("\n", '<br/>')}</code></pre>"
end
def list(contents, _list_type)
"<p>#{contents}</p>"
end
def list_item(text, _list_type)
"- #{text.strip}<br />"
end
def autolink(link, link_type)
return link if link_type == :email
"<a href=\"#{link}\" rel=\"noopener noreferrer\" target=\"_blank\">#{link}</a>"
end
private
def html_entities
@html_entities ||= HTMLEntities.new
end
def encode(html)
html_entities.encode(html)
end
end
class Formatter
include Singleton
include RoutingHelper
include ActionView::Helpers::TextHelper
def format(status, **options)
if status.respond_to?(:reblog?) && status.reblog?
prepend_reblog = status.reblog.account.acct
status = status.proper
else
prepend_reblog = false
end
raw_content = status.text
if options[:inline_poll_options] && status.preloadable_poll
raw_content = raw_content + "\n\n" + status.preloadable_poll.options.map { |title| "[ ] #{title}" }.join("\n")
end
return '' if raw_content.blank?
unless status.local?
html = reformat(raw_content)
html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify]
return html.html_safe # rubocop:disable Rails/OutputSafety
end
linkable_accounts = status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []
linkable_accounts << status.account
html = raw_content
html = "RT @#{prepend_reblog} #{html}" if prepend_reblog
html = encode_and_link_urls(html, linkable_accounts, {}, status.content_type == 'text/markdown')
html = format_markdown(html) if status.content_type == 'text/markdown'
html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify]
unless %w(text/markdown text/html).include?(status.content_type)
html = simple_format(html, {}, sanitize: false)
html = html.delete("\n")
end
html.html_safe # rubocop:disable Rails/OutputSafety
end
def reformat(html)
sanitize(html, Sanitize::Config::MASTODON_STRICT)
rescue ArgumentError
''
end
def plaintext(status)
return status.text if status.local?
text = status.text.gsub(/(<br \/>|<br>|<\/p>)+/) { |match| "#{match}\n" }
strip_tags(text)
end
def simplified_format(account, **options)
html = account.local? ? linkify(account.note) : reformat(account.note)
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
end
def sanitize(html, config)
Sanitize.fragment(html, config)
end
def format_markdown(html)
html = reformat(markdown_formatter.render(html))
html.delete("\r").delete("\n")
end
def format_spoiler(status, **options)
html = encode(status.spoiler_text)
html = encode_custom_emojis(html, status.emojis, options[:autoplay])
html.html_safe # rubocop:disable Rails/OutputSafety
end
def format_poll_option(status, option, **options)
html = encode(option.title)
html = encode_custom_emojis(html, status.emojis, options[:autoplay])
html.html_safe # rubocop:disable Rails/OutputSafety
end
def format_display_name(account, **options)
html = encode(account.display_name.presence || account.username)
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
end
def format_field(account, str, **options)
html = account.local? ? encode_and_link_urls(str, me: true, with_domain: true) : reformat(str)
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
end
def linkify(text)
html = encode_and_link_urls(text)
html = simple_format(html, {}, sanitize: false)
html = html.delete("\n")
html.html_safe # rubocop:disable Rails/OutputSafety
end
private
def markdown_formatter
return @markdown_formatter if defined?(@markdown_formatter)
extensions = {
filter_html: true,
no_links: false,
no_styles: true,
autolink: false,
no_intra_emphasis: true,
fenced_code_blocks: true,
disable_indented_code_blocks: true,
strikethrough: true,
lax_spacing: true,
space_after_headers: true,
superscript: true,
underline: true,
highlight: true,
footnotes: false,
}
renderer = HTMLRenderer.new(
filter_html: false,
escape_html: false,
no_images: true,
no_styles: true,
safe_links_only: true,
hard_wrap: true,
link_attributes: { target: '_blank', rel: 'nofollow noopener' }
)
@markdown_formatter = Redcarpet::Markdown.new(renderer, extensions)
end
def html_entities
@html_entities ||= HTMLEntities.new
end
def encode(html)
html_entities.encode(html)
end
def encode_and_link_urls(html, accounts = nil, options = {}, markdown = false)
entities = utf8_friendly_extractor(html, extract_url_without_protocol: false)
if accounts.is_a?(Hash)
options = accounts
accounts = nil
end
rewrite(html.dup, entities) do |entity|
if entity[:url]
if markdown
entity[:url].to_s
else
link_to_url(entity, options)
end
elsif entity[:hashtag]
link_to_hashtag(entity)
elsif entity[:screen_name]
link_to_mention(entity, accounts, options)
end
end
end
def count_tag_nesting(tag)
if tag[1] == '/'
-1
elsif tag[-2] == '/'
0
else
1
end
end
# rubocop:disable Metrics/BlockNesting
def encode_custom_emojis(html, emojis, animate = false)
return html if emojis.empty?
emoji_map = emojis.each_with_object({}) { |e, h| h[e.shortcode] = [full_asset_url(e.image.url), full_asset_url(e.image.url(:static))] }
i = -1
tag_open_index = nil
inside_shortname = false
shortname_start_index = -1
invisible_depth = 0
while i + 1 < html.size
i += 1
if invisible_depth.zero? && inside_shortname && html[i] == ':'
shortcode = html[shortname_start_index + 1..i - 1]
emoji = emoji_map[shortcode]
if emoji
original_url, static_url = emoji
replacement = begin
if animate
image_tag(original_url, draggable: false, class: 'emojione', alt: ":#{shortcode}:", title: ":#{shortcode}:")
else
image_tag(original_url, draggable: false, class: 'emojione custom-emoji', alt: ":#{shortcode}:", title: ":#{shortcode}:", data: { original: original_url, static: static_url })
end
end
before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : ''
html = before_html + replacement + html[i + 1..-1]
i += replacement.size - (shortcode.size + 2) - 1
else
i -= 1
end
inside_shortname = false
elsif tag_open_index && html[i] == '>'
tag = html[tag_open_index..i]
tag_open_index = nil
if invisible_depth.positive?
invisible_depth += count_tag_nesting(tag)
elsif tag == '<span class="invisible">'
invisible_depth = 1
end
elsif html[i] == '<'
tag_open_index = i
inside_shortname = false
elsif !tag_open_index && html[i] == ':'
inside_shortname = true
shortname_start_index = i
end
end
html
end
# rubocop:enable Metrics/BlockNesting
def rewrite(text, entities)
text = text.to_s
# Sort by start index
entities = entities.sort_by do |entity|
indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
indices.first
end
result = []
last_index = entities.reduce(0) do |index, entity|
indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
result << encode(text[index...indices.first])
result << yield(entity)
indices.last
end
result << encode(text[last_index..-1])
result.flatten.join
end
def utf8_friendly_extractor(text, options = {})
# Note: I couldn't obtain list_slug with @user/list-name format
# for mention so this requires additional check
special = Extractor.extract_urls_with_indices(text, options)
standard = Extractor.extract_entities_with_indices(text, options)
extra = Extractor.extract_extra_uris_with_indices(text)
Extractor.remove_overlapping_entities(special + standard + extra)
end
def link_to_url(entity, options = {})
url = Addressable::URI.parse(entity[:url])
html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' }
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
Twitter::TwitterText::Autolink.send(:link_to_text, entity, link_html(entity[:url]), url, html_attrs)
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
encode(entity[:url])
end
def link_to_mention(entity, linkable_accounts, options = {})
acct = entity[:screen_name]
return link_to_account(acct, options) unless linkable_accounts
same_username_hits = 0
account = nil
username, domain = acct.split('@')
domain = nil if TagManager.instance.local_domain?(domain)
linkable_accounts.each do |item|
same_username = item.username.casecmp(username).zero?
same_domain = item.domain.nil? ? domain.nil? : item.domain.casecmp(domain)&.zero?
if same_username && !same_domain
same_username_hits += 1
elsif same_username && same_domain
account = item
end
end
account ? mention_html(account, with_domain: same_username_hits.positive? || options[:with_domain]) : "@#{encode(acct)}"
end
def link_to_account(acct, options = {})
username, domain = acct.split('@')
domain = nil if TagManager.instance.local_domain?(domain)
account = EntityCache.instance.mention(username, domain)
account ? mention_html(account, with_domain: options[:with_domain]) : "@#{encode(acct)}"
end
def link_to_hashtag(entity)
hashtag_html(entity[:hashtag])
end
def link_html(url)
url = Addressable::URI.parse(url).to_s
prefix = url.match(/\A(https?:\/\/(www\.)?|xmpp:)/).to_s
text = url[prefix.length, 30]
suffix = url[prefix.length + 30..-1]
cutoff = url[prefix.length..-1].length > 30
"<span class=\"invisible\">#{encode(prefix)}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{encode(text)}</span><span class=\"invisible\">#{encode(suffix)}</span>"
end
def hashtag_html(tag)
"<a href=\"#{encode(tag_url(tag))}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{encode(tag)}</span></a>"
end
def mention_html(account, with_domain: false)
"<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(with_domain ? account.pretty_acct : account.username)}</span></a></span>"
end
end

2
app/serializers/rest/status_serializer.rb

@ -73,7 +73,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
end
def content
status_content_format(object)
Formatter.instance.format(object)
end
def url

3
config/locales/ku.yml

@ -1611,7 +1611,8 @@ ku:
title: "%{instance} mercên bikaranîn û politîkayên nehêniyê"
themes:
contrast: Mastodon (Dijberiya bilind)
default: Mastodon (Tarî)
mastodon-dark: Mastodon (Tarî)
default: Night