Merge tag 'v1.6.0rc3' into sync/upstream

This commit is contained in:
David Yip 2017-09-09 14:28:08 -05:00
commit 514fc908a3
87 changed files with 947 additions and 285 deletions

View file

@ -26,7 +26,7 @@ LOCAL_HTTPS=true
# ALTERNATE_DOMAINS=example1.com,example2.com # ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets # Application secrets
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose) # Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
PAPERCLIP_SECRET= PAPERCLIP_SECRET=
SECRET_KEY_BASE= SECRET_KEY_BASE=
OTP_SECRET= OTP_SECRET=
@ -36,7 +36,7 @@ OTP_SECRET=
# You should only generate this once per instance. If you later decide to change it, all push subscription will # You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe. # be invalidated, requiring the users to access the website again to resubscribe.
# #
# Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose) # Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
# #
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html # For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY= VAPID_PRIVATE_KEY=
@ -98,6 +98,15 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_ENDPOINT= # S3_ENDPOINT=
# S3_SIGNATURE_VERSION= # S3_SIGNATURE_VERSION=
# Swift (optional)
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# SWIFT_AUTH_URL=
# SWIFT_CONTAINER=
# SWIFT_OBJECT_URL=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST= # S3_CLOUDFRONT_HOST=

View file

@ -15,6 +15,7 @@ gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2' gem 'dotenv-rails', '~> 2.2'
gem 'aws-sdk', '~> 2.9' gem 'aws-sdk', '~> 2.9'
gem 'fog-openstack', '~> 0.1'
gem 'paperclip', '~> 5.1' gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6' gem 'paperclip-av-transcoder', '~> 0.6'

View file

@ -154,12 +154,25 @@ GEM
erubis (2.7.0) erubis (2.7.0)
et-orbi (1.0.5) et-orbi (1.0.5)
tzinfo tzinfo
excon (0.58.0)
execjs (2.7.0) execjs (2.7.0)
fabrication (2.16.2) fabrication (2.16.2)
faker (1.7.3) faker (1.7.3)
i18n (~> 0.5) i18n (~> 0.5)
fast_blank (1.0.0) fast_blank (1.0.0)
ffi (1.9.18) ffi (1.9.18)
fog-core (1.45.0)
builder
excon (~> 0.58)
formatador (~> 0.2)
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
fog-openstack (0.1.21)
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
fuubar (2.2.0) fuubar (2.2.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
@ -211,6 +224,7 @@ GEM
rainbow (~> 2.2) rainbow (~> 2.2)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.0) idn-ruby (0.1.0)
ipaddress (0.8.3)
jmespath (1.3.1) jmespath (1.3.1)
json (2.1.0) json (2.1.0)
json-ld (2.1.5) json-ld (2.1.5)
@ -535,6 +549,7 @@ DEPENDENCIES
fabrication (~> 2.16) fabrication (~> 2.16)
faker (~> 1.7) faker (~> 1.7)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fog-openstack (~> 0.1)
fuubar (~> 2.2) fuubar (~> 2.2)
goldfinger (~> 2.0) goldfinger (~> 2.0)
hamlit-rails (~> 0.2) hamlit-rails (~> 0.2)

View file

@ -14,7 +14,7 @@ class AccountsController < ApplicationController
return return
end end
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) unless media_requested? @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
@next_url = next_url unless @statuses.empty? @next_url = next_url unless @statuses.empty?
@ -22,7 +22,7 @@ class AccountsController < ApplicationController
format.atom do format.atom do
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id]) @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.to_a)) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end end
format.json do format.json do
@ -33,6 +33,10 @@ class AccountsController < ApplicationController
private private
def show_pinned_statuses?
[replies_requested?, media_requested?, params[:max_id].present?, params[:since_id].present?].none?
end
def filtered_statuses def filtered_statuses
default_statuses.tap do |statuses| default_statuses.tap do |statuses|
statuses.merge!(only_media_scope) if media_requested? statuses.merge!(only_media_scope) if media_requested?

View file

@ -26,8 +26,12 @@ class ActivityPub::InboxesController < Api::BaseController
end end
def upgrade_account def upgrade_account
return unless signed_request_account.subscribed? if signed_request_account.ostatus?
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) signed_request_account.update(last_webfingered_at: nil)
ResolveRemoteAccountWorker.perform_async(signed_request_account.acct)
end
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
end end
def process_payload def process_payload

View file

@ -14,6 +14,16 @@ class Api::V1::AccountsController < Api::BaseController
def follow def follow
FollowService.new.call(current_user.account, @account.acct) FollowService.new.call(current_user.account, @account.acct)
unless @account.locked?
relationships = AccountRelationshipsPresenter.new(
[@account.id],
current_user.account_id,
following_map: { @account.id => true },
requested_map: { @account.id => false }
)
end
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end end

View file

@ -12,8 +12,14 @@ module RoutingHelper
end end
def full_asset_url(source, options = {}) def full_asset_url(source, options = {})
source = ActionController::Base.helpers.asset_url(source, options) unless Rails.configuration.x.use_s3 source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
URI.join(root_url, source).to_s URI.join(root_url, source).to_s
end end
private
def use_storage?
Rails.configuration.x.use_s3 || Rails.configuration.x.use_swift
end
end end

View file

@ -5,6 +5,7 @@ import IntersectionObserverArticle from './intersection_observer_article';
import LoadMore from './load_more'; import LoadMore from './load_more';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable';
export default class ScrollableList extends PureComponent { export default class ScrollableList extends PureComponent {
@ -95,7 +96,12 @@ export default class ScrollableList extends PureComponent {
getFirstChildKey (props) { getFirstChildKey (props) {
const { children } = props; const { children } = props;
const firstChild = Array.isArray(children) ? children[0] : children; let firstChild = children;
if (children instanceof ImmutableList) {
firstChild = children.get(0);
} else if (Array.isArray(children)) {
firstChild = children[0];
}
return firstChild && firstChild.key; return firstChild && firstChild.key;
} }

View file

@ -149,7 +149,7 @@ export default class VideoPlayer extends React.PureComponent {
if (!this.state.visible) { if (!this.state.visible) {
if (sensitive) { if (sensitive) {
return ( return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}> <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler__video' onClick={this.handleVisibility}>
{spoilerButton} {spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> <span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@ -157,7 +157,7 @@ export default class VideoPlayer extends React.PureComponent {
); );
} else { } else {
return ( return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}> <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler__video' onClick={this.handleVisibility}>
{spoilerButton} {spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> <span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>

View file

@ -77,6 +77,7 @@ export default class Favourites extends ImmutablePureComponent {
onClick={this.handleHeaderClick} onClick={this.handleHeaderClick}
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
showBackButton
/> />
<StatusList <StatusList

View file

@ -124,6 +124,7 @@ export default class Notifications extends React.PureComponent {
const scrollContainer = ( const scrollContainer = (
<ScrollableList <ScrollableList
scrollKey={`notifications-${columnId}`} scrollKey={`notifications-${columnId}`}
trackScroll={!pinned}
isLoading={isLoading} isLoading={isLoading}
hasMore={hasMore} hasMore={hasMore}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

View file

@ -26,12 +26,12 @@
"bundle_modal_error.close": "Schließen", "bundle_modal_error.close": "Schließen",
"bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.", "bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.",
"bundle_modal_error.retry": "Erneut versuchen", "bundle_modal_error.retry": "Erneut versuchen",
"column.blocks": "Blockierte Benutzer", "column.blocks": "Blockierte Profile",
"column.community": "Lokale Zeitleiste", "column.community": "Lokale Zeitleiste",
"column.favourites": "Favoriten", "column.favourites": "Favoriten",
"column.follow_requests": "Folgeanfragen", "column.follow_requests": "Folgeanfragen",
"column.home": "Startseite", "column.home": "Startseite",
"column.mutes": "Stummgeschaltete Benutzer", "column.mutes": "Stummgeschaltete Profile",
"column.notifications": "Mitteilungen", "column.notifications": "Mitteilungen",
"column.public": "Gesamtes bekanntes Netz", "column.public": "Gesamtes bekanntes Netz",
"column_back_button.label": "Zurück", "column_back_button.label": "Zurück",
@ -46,7 +46,7 @@
"compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Jeder kann dir jederzeit folgen, um deine privaten Beiträge einzusehen.", "compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Jeder kann dir jederzeit folgen, um deine privaten Beiträge einzusehen.",
"compose_form.lock_disclaimer.lock": "gesperrt", "compose_form.lock_disclaimer.lock": "gesperrt",
"compose_form.placeholder": "Worüber möchtest du schreiben?", "compose_form.placeholder": "Worüber möchtest du schreiben?",
"compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Benutzer auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.", "compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Profile auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.",
"compose_form.publish": "Tröt", "compose_form.publish": "Tröt",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Medien als heikel markieren", "compose_form.sensitive": "Medien als heikel markieren",
@ -77,18 +77,18 @@
"emoji_button.travel": "Reise und Orte", "emoji_button.travel": "Reise und Orte",
"empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!", "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!",
"empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.", "empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.",
"empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Benutzer anzutreffen.", "empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Profile zu finden.",
"empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv gewesen bist, wird sie für dich so schnell wie möglich wiedererstellt.", "empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv gewesen bist, wird sie für dich so schnell wie möglich wiedererstellt.",
"empty_column.home.public_timeline": "die öffentliche Zeitleiste", "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
"empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.", "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.",
"empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Benutzern von anderen Instanzen, um es aufzufüllen.", "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um es aufzufüllen.",
"follow_request.authorize": "Erlauben", "follow_request.authorize": "Erlauben",
"follow_request.reject": "Ablehnen", "follow_request.reject": "Ablehnen",
"getting_started.appsshort": "Anwendungen", "getting_started.appsshort": "Anwendungen",
"getting_started.faq": "Häufig gestellte Fragen", "getting_started.faq": "Häufig gestellte Fragen",
"getting_started.heading": "Erste Schritte", "getting_started.heading": "Erste Schritte",
"getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.", "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
"getting_started.userguide": "Nutzeranleitung", "getting_started.userguide": "Bedienungsanleitung",
"home.column_settings.advanced": "Fortgeschritten", "home.column_settings.advanced": "Fortgeschritten",
"home.column_settings.basic": "Einfach", "home.column_settings.basic": "Einfach",
"home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke", "home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke",
@ -101,14 +101,14 @@
"loading_indicator.label": "Lade…", "loading_indicator.label": "Lade…",
"media_gallery.toggle_visible": "Sichtbarkeit einstellen", "media_gallery.toggle_visible": "Sichtbarkeit einstellen",
"missing_indicator.label": "Nicht gefunden", "missing_indicator.label": "Nicht gefunden",
"navigation_bar.blocks": "Blockierte Benutzer", "navigation_bar.blocks": "Blockierte Profile",
"navigation_bar.community_timeline": "Lokale Zeitleiste", "navigation_bar.community_timeline": "Lokale Zeitleiste",
"navigation_bar.edit_profile": "Profil bearbeiten", "navigation_bar.edit_profile": "Profil bearbeiten",
"navigation_bar.favourites": "Favoriten", "navigation_bar.favourites": "Favoriten",
"navigation_bar.follow_requests": "Folgeanfragen", "navigation_bar.follow_requests": "Folgeanfragen",
"navigation_bar.info": "Erweiterte Informationen", "navigation_bar.info": "Erweiterte Informationen",
"navigation_bar.logout": "Abmelden", "navigation_bar.logout": "Abmelden",
"navigation_bar.mutes": "Stummgeschaltete Benutzer", "navigation_bar.mutes": "Stummgeschaltete Profile",
"navigation_bar.preferences": "Einstellungen", "navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Föderierte Zeitleiste", "navigation_bar.public_timeline": "Föderierte Zeitleiste",
"notification.favourite": "{name} favorisierte deinen Status", "notification.favourite": "{name} favorisierte deinen Status",
@ -132,7 +132,7 @@
"onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.", "onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.",
"onboarding.page_four.notifications": "Wenn jemand mir dir interagiert, bekommst du eine Mitteilung.", "onboarding.page_four.notifications": "Wenn jemand mir dir interagiert, bekommst du eine Mitteilung.",
"onboarding.page_one.federation": "Mastodon ist ein soziales Netzwerk, das aus unabhängigen Servern besteht. Diese Server nennen wir auch Instanzen.", "onboarding.page_one.federation": "Mastodon ist ein soziales Netzwerk, das aus unabhängigen Servern besteht. Diese Server nennen wir auch Instanzen.",
"onboarding.page_one.handle": "Du bist auf der Instanz {domain}, also ist dein vollständiger Nutzername im Netzwerk {handle}", "onboarding.page_one.handle": "Du bist auf der Instanz {domain}, also ist dein vollständiger Profilname im Netzwerk {handle}",
"onboarding.page_one.welcome": "Willkommen bei Mastodon!", "onboarding.page_one.welcome": "Willkommen bei Mastodon!",
"onboarding.page_six.admin": "Für deine Instanz ist {admin} zuständig.", "onboarding.page_six.admin": "Für deine Instanz ist {admin} zuständig.",
"onboarding.page_six.almost_done": "Fast fertig…", "onboarding.page_six.almost_done": "Fast fertig…",
@ -143,11 +143,11 @@
"onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!", "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!",
"onboarding.page_six.various_app": "mobile Anwendungen", "onboarding.page_six.various_app": "mobile Anwendungen",
"onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen oder deine Beschreibung anzupassen. Dort findest du auch andere Einstellungen.", "onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen oder deine Beschreibung anzupassen. Dort findest du auch andere Einstellungen.",
"onboarding.page_three.search": "Benutze die Suchfunktion, um Leute oder Themen zu finden. Zum Beispiel, die Hashtags {illustration} oder {introductions}. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Nutzernamen.", "onboarding.page_three.search": "Benutze die Suchfunktion, um Leute oder Themen zu finden. Zum Beispiel, die Hashtags {illustration} oder {introductions}. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Profilnamen.",
"onboarding.page_two.compose": "Schreibe Beiträge aus der Schreiben-Spalte. Du kannst Bilder und kurze Videos hochladen, Sichtbarkeitseinstellungen ändern und Inhaltswarnungen hinzufügen.", "onboarding.page_two.compose": "Schreibe Beiträge aus der Schreiben-Spalte. Du kannst Bilder und kurze Videos hochladen, Sichtbarkeitseinstellungen ändern und Inhaltswarnungen hinzufügen.",
"onboarding.skip": "Überspringen", "onboarding.skip": "Überspringen",
"privacy.change": "Privatsphäre des Status anpassen", "privacy.change": "Privatsphäre des Status anpassen",
"privacy.direct.long": "Beitrag nur an erwähnte Benutzer", "privacy.direct.long": "Beitrag nur an erwähnte Profile",
"privacy.direct.short": "Direkt", "privacy.direct.short": "Direkt",
"privacy.private.long": "Beitrag nur an Folgende", "privacy.private.long": "Beitrag nur an Folgende",
"privacy.private.short": "Privat", "privacy.private.short": "Privat",

View file

@ -63,8 +63,8 @@
"confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟", "confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟",
"confirmations.unfollow.confirm": "لغو پیگیری", "confirmations.unfollow.confirm": "لغو پیگیری",
"confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟", "confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "برای جاگذاری این نوشته در سایت خودتان، کد زیر را کپی کنید.",
"embed.preview": "Here is what it will look like:", "embed.preview": "نوشتهٔ جاگذاری‌شده این گونه به نظر خواهد رسید:",
"emoji_button.activity": "فعالیت", "emoji_button.activity": "فعالیت",
"emoji_button.flags": "پرچم‌ها", "emoji_button.flags": "پرچم‌ها",
"emoji_button.food": "غذا و نوشیدنی", "emoji_button.food": "غذا و نوشیدنی",
@ -164,14 +164,14 @@
"standalone.public_title": "نگاهی به کاربران این سرور...", "standalone.public_title": "نگاهی به کاربران این سرور...",
"status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید", "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
"status.delete": "پاک‌کردن", "status.delete": "پاک‌کردن",
"status.embed": "Embed", "status.embed": "جاگذاری",
"status.favourite": "پسندیدن", "status.favourite": "پسندیدن",
"status.load_more": "بیشتر نشان بده", "status.load_more": "بیشتر نشان بده",
"status.media_hidden": "تصویر پنهان شده", "status.media_hidden": "تصویر پنهان شده",
"status.mention": "نام‌بردن از @{name}", "status.mention": "نام‌بردن از @{name}",
"status.mute_conversation": "بی‌صداکردن گفتگو", "status.mute_conversation": "بی‌صداکردن گفتگو",
"status.open": "این نوشته را باز کن", "status.open": "این نوشته را باز کن",
"status.pin": "Pin on profile", "status.pin": "نوشتهٔ ثابت نمایه",
"status.reblog": "بازبوقیدن", "status.reblog": "بازبوقیدن",
"status.reblogged_by": "{name} بازبوقید", "status.reblogged_by": "{name} بازبوقید",
"status.reply": "پاسخ", "status.reply": "پاسخ",
@ -183,7 +183,7 @@
"status.show_less": "نهفتن", "status.show_less": "نهفتن",
"status.show_more": "نمایش", "status.show_more": "نمایش",
"status.unmute_conversation": "باصداکردن گفتگو", "status.unmute_conversation": "باصداکردن گفتگو",
"status.unpin": "Unpin from profile", "status.unpin": "برداشتن نوشتهٔ ثابت نمایه",
"tabs_bar.compose": "بنویسید", "tabs_bar.compose": "بنویسید",
"tabs_bar.federated_timeline": "همگانی", "tabs_bar.federated_timeline": "همگانی",
"tabs_bar.home": "خانه", "tabs_bar.home": "خانه",

View file

@ -1,7 +1,7 @@
{ {
"account.block": "Blokiraj @{name}", "account.block": "Blokiraj @{name}",
"account.block_domain": "Sakrij sve sa {domain}", "account.block_domain": "Sakrij sve sa {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.",
"account.edit_profile": "Uredi profil", "account.edit_profile": "Uredi profil",
"account.follow": "Slijedi", "account.follow": "Slijedi",
"account.followers": "Sljedbenici", "account.followers": "Sljedbenici",
@ -15,7 +15,7 @@
"account.requested": "Čeka pristanak", "account.requested": "Čeka pristanak",
"account.share": "Share @{name}'s profile", "account.share": "Share @{name}'s profile",
"account.unblock": "Deblokiraj @{name}", "account.unblock": "Deblokiraj @{name}",
"account.unblock_domain": "Otkrij {domain}", "account.unblock_domain": "Poništi sakrivanje {domain}",
"account.unfollow": "Prestani slijediti", "account.unfollow": "Prestani slijediti",
"account.unmute": "Poništi utišavanje @{name}", "account.unmute": "Poništi utišavanje @{name}",
"account.view_full_profile": "View full profile", "account.view_full_profile": "View full profile",
@ -43,7 +43,7 @@
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigacija", "column_subheading.navigation": "Navigacija",
"column_subheading.settings": "Postavke", "column_subheading.settings": "Postavke",
"compose_form.lock_disclaimer": "Tvoj račun nije {locked}. Svatko te može slijediti i vidjeti tvoje postove namijenjene samo sljedbenicima.", "compose_form.lock_disclaimer": "Tvoj račun nije {locked}. Svatko te može slijediti kako bi vidio postove namijenjene samo tvojim sljedbenicima.",
"compose_form.lock_disclaimer.lock": "zaključan", "compose_form.lock_disclaimer.lock": "zaključan",
"compose_form.placeholder": "Što ti je na umu?", "compose_form.placeholder": "Što ti je na umu?",
"compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bi biti podignut ili biti učinjen vidljivim na drugi način neželjenim primateljima.", "compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bi biti podignut ili biti učinjen vidljivim na drugi način neželjenim primateljima.",
@ -54,13 +54,14 @@
"compose_form.spoiler_placeholder": "Upozorenje o sadržaju", "compose_form.spoiler_placeholder": "Upozorenje o sadržaju",
"confirmation_modal.cancel": "Otkaži", "confirmation_modal.cancel": "Otkaži",
"confirmations.block.confirm": "Blokiraj", "confirmations.block.confirm": "Blokiraj",
"confirmations.block.message": "Jesi li siguran da želiš blokirati {name}?", "confirmations.block.message": "Želiš li sigurno blokirati {name}?",
"confirmations.delete.confirm": "Obriši", "confirmations.delete.confirm": "Obriši",
"confirmations.delete.message": "Jesi li siguran da želiš obrisati ovaj status?", "confirmations.delete.message": "Želiš li stvarno obrisati ovaj status?",
"confirmations.domain_block.confirm": "Sakrij cijelu domenu", "confirmations.domain_block.confirm": "Sakrij cijelu domenu",
"confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš blokirati sve sa {domain}? U većini slučajeva nekoliko ciljanih blokiranja ili utišavanja je dostatno i poželjnije.", "confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Utišaj", "confirmations.mute.confirm": "Utišaj",
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?", "confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
@ -69,16 +70,16 @@
"emoji_button.flags": "Zastave", "emoji_button.flags": "Zastave",
"emoji_button.food": "Hrana & Piće", "emoji_button.food": "Hrana & Piće",
"emoji_button.label": "Umetni smajlije", "emoji_button.label": "Umetni smajlije",
"emoji_button.nature": "Nature", "emoji_button.nature": "Priroda",
"emoji_button.objects": "Objekti", "emoji_button.objects": "Objekti",
"emoji_button.people": "Ljudi", "emoji_button.people": "Ljudi",
"emoji_button.search": "Traži...", "emoji_button.search": "Traži...",
"emoji_button.symbols": "Simboli", "emoji_button.symbols": "Simboli",
"emoji_button.travel": "Putovanja i Mjesta", "emoji_button.travel": "Putovanja & Mjesta",
"empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!", "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!",
"empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.", "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.",
"empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.", "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.",
"empty_column.home.inactivity": "Tvoj home feed je prazan. Ako si neko vrijeme bio neaktivan, regenerirat će se uskoro.", "empty_column.home.inactivity": "Tvoj home feed je prazan. Ako si neko vrijeme bio neaktivan, uskoro ćese regenerirati.",
"empty_column.home.public_timeline": "javni timeline", "empty_column.home.public_timeline": "javni timeline",
"empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.", "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.",
"empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio", "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio",
@ -88,11 +89,11 @@
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "Počnimo", "getting_started.heading": "Počnimo",
"getting_started.open_source_notice": "Mastodon je softver otvorenog koda. Možeš pridonijeti ili prijaviti probleme na GitHubu {github}.", "getting_started.open_source_notice": "Mastodon je softver otvorenog koda. Možeš pridonijeti ili prijaviti probleme na GitHubu {github}.",
"getting_started.userguide": "Vodič za korisnike", "getting_started.userguide": "Upute za korištenje",
"home.column_settings.advanced": "Napredno", "home.column_settings.advanced": "Napredno",
"home.column_settings.basic": "Osnovno", "home.column_settings.basic": "Osnovno",
"home.column_settings.filter_regex": "Filtriraj s regularnim izrazima", "home.column_settings.filter_regex": "Filtriraj s regularnim izrazima",
"home.column_settings.show_reblogs": "Pokaži boosts", "home.column_settings.show_reblogs": "Pokaži boostove",
"home.column_settings.show_replies": "Pokaži odgovore", "home.column_settings.show_replies": "Pokaži odgovore",
"home.settings": "Postavke Stupca", "home.settings": "Postavke Stupca",
"lightbox.close": "Zatvori", "lightbox.close": "Zatvori",
@ -113,7 +114,7 @@
"navigation_bar.public_timeline": "Federalni timeline", "navigation_bar.public_timeline": "Federalni timeline",
"notification.favourite": "{name} je lajkao tvoj status", "notification.favourite": "{name} je lajkao tvoj status",
"notification.follow": "{name} te sada slijedi", "notification.follow": "{name} te sada slijedi",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} te je spomenuo",
"notification.reblog": "{name} je podigao tvoj status", "notification.reblog": "{name} je podigao tvoj status",
"notifications.clear": "Očisti notifikacije", "notifications.clear": "Očisti notifikacije",
"notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?", "notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?",
@ -123,28 +124,28 @@
"notifications.column_settings.mention": "Spominjanja:", "notifications.column_settings.mention": "Spominjanja:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.reblog": "Boostovi:",
"notifications.column_settings.show": "Prikaži u stupcu", "notifications.column_settings.show": "Prikaži u stupcu",
"notifications.column_settings.sound": "Sviraj zvuk", "notifications.column_settings.sound": "Sviraj zvuk",
"onboarding.done": "Učinjeno", "onboarding.done": "Učinjeno",
"onboarding.next": "Sljedeća", "onboarding.next": "Sljedeće",
"onboarding.page_five.public_timelines": "The local timeline prikazuje javne postove svih na {domain}. Federalni timeline pokazuje javne postove svih sa {domain} domena koje slijediš. To je sjajan način da otkriješ nove ljude.", "onboarding.page_five.public_timelines": "Lokalni timeline prikazuje javne postove sviju od svakog na {domain}. Federalni timeline prikazuje javne postove svakog koga ljudi na {domain} slijede. To su Javni Timelineovi, sjajan način za otkriti nove ljude.",
"onboarding.page_four.home": "The home timeline prikazuje samo postove ljudi koje slijediš.", "onboarding.page_four.home": "The home timeline prikazuje postove ljudi koje slijediš.",
"onboarding.page_four.notifications": "Stupac notifikacija pokazuje kada je netko u interakciji s tobom.", "onboarding.page_four.notifications": "Stupac za notifikacije pokazuje poruke drugih upućene tebi.",
"onboarding.page_one.federation": "Mastodon je mreža nezavisnih servera udruženih kako bi stvorili veću socijalnu mrežu. Te servere zovemo instance.", "onboarding.page_one.federation": "Mastodon čini mreža neovisnih servera udruženih u jednu veću socialnu mrežu. Te servere nazivamo instancama.",
"onboarding.page_one.handle": "Ti si na {domain}, tako da je tvoj potpuni opis {handle}", "onboarding.page_one.handle": "Ti si na {domain}, i tvoja puna handle je {handle}",
"onboarding.page_one.welcome": "Dobro došli u Mastodon!", "onboarding.page_one.welcome": "Dobro došli na Mastodon!",
"onboarding.page_six.admin": "Administrator tvoje instance je {admin}.", "onboarding.page_six.admin": "Administrator tvoje instance je {admin}.",
"onboarding.page_six.almost_done": "Još malo pa gotovo...", "onboarding.page_six.almost_done": "Još malo pa gotovo...",
"onboarding.page_six.appetoot": "Živjeli!", "onboarding.page_six.appetoot": "Živjeli!",
"onboarding.page_six.apps_available": "Postoje {apps} dostupne za iOS, Android i druge platforme.", "onboarding.page_six.apps_available": "Postoje {apps} dostupne za iOS, Android i druge platforme.",
"onboarding.page_six.github": "Mastodon je besplatan softver otvorenog koda. Možeš prijaviti greške, zahtijevati mogućnosti, ili pridonijeti kodu na {github}.", "onboarding.page_six.github": "Mastodon je besplatan softver otvorenog koda. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "smjernice zajednice", "onboarding.page_six.guidelines": "smjernice zajednice",
"onboarding.page_six.read_guidelines": "Molimo, pročitaj {domain}'s {guidelines}!", "onboarding.page_six.read_guidelines": "Molimo pročitaj {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobilne aplikacije", "onboarding.page_six.various_app": "mobilne aplikacije",
"onboarding.page_three.profile": "Uredi svoj profil mijenjanjem avatara, biografije i imena koje će biti prikazano. Naći ćeš i druge korisne postavke.", "onboarding.page_three.profile": "Uredi svoj profil promjenom svog avatara, biografije, i imena. Ovdje ćeš isto tako pronaći i druge postavke.",
"onboarding.page_three.search": "Koristi tražilicu kako bi pronašao ljude i sadržaj sa određenim hashtagovima, kao što su {illustration} i {introductions}. Da bi našao osobu koja nije na ovoj instanci, upotrijebi njihov puni opis.", "onboarding.page_three.search": "Koristi tražilicu kako bi pronašao ljude i tražio hashtags, kao što su {illustration} i {introductions}. Kako bi pronašao osobu koja nije na ovoj instanci, upotrijebi njen pun handle.",
"onboarding.page_two.compose": "Piši postove u stupcu za njihovo sastavljanje. Možeš uploadati slike, promijeniti postavke privatnosti, i dodati upozorenja o sadržaju s ikonama ispod.", "onboarding.page_two.compose": "Piši postove u stupcu za sastavljanje. Možeš uploadati slike, promijeniti postavke privatnosti, i dodati upozorenja o sadržaju s ikonama ispod.",
"onboarding.skip": "Preskoči", "onboarding.skip": "Preskoči",
"privacy.change": "Podesi status privatnosti", "privacy.change": "Podesi status privatnosti",
"privacy.direct.long": "Prikaži samo spomenutim korisnicima", "privacy.direct.long": "Prikaži samo spomenutim korisnicima",
@ -162,7 +163,7 @@
"search.placeholder": "Traži", "search.placeholder": "Traži",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}", "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "Ovaj post ne može biti podignut", "status.cannot_reblog": "Ovaj post ne može biti boostan",
"status.delete": "Obriši", "status.delete": "Obriši",
"status.embed": "Embed", "status.embed": "Embed",
"status.favourite": "Označi omiljenim", "status.favourite": "Označi omiljenim",
@ -196,5 +197,5 @@
"video_player.expand": "Proširi video", "video_player.expand": "Proširi video",
"video_player.toggle_sound": "Toggle zvuk", "video_player.toggle_sound": "Toggle zvuk",
"video_player.toggle_visible": "Preklopi vidljivost", "video_player.toggle_visible": "Preklopi vidljivost",
"video_player.video_error": "Video nije mogao biti prikazan" "video_player.video_error": "Video ne može biti reproduciran"
} }

View file

@ -12,7 +12,7 @@
"account.mute": "Rescondre @{name}", "account.mute": "Rescondre @{name}",
"account.posts": "Estatuts", "account.posts": "Estatuts",
"account.report": "Senhalar @{name}", "account.report": "Senhalar @{name}",
"account.requested": "Invitacion mandada", "account.requested": "Invitacion mandada. Clicatz per anullar.",
"account.share": "Partejar lo perfil a @{name}", "account.share": "Partejar lo perfil a @{name}",
"account.unblock": "Desblocar @{name}", "account.unblock": "Desblocar @{name}",
"account.unblock_domain": "Desblocar {domain}", "account.unblock_domain": "Desblocar {domain}",
@ -63,8 +63,8 @@
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name}?", "confirmations.mute.message": "Sètz segur de voler metre en silenci {name}?",
"confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name}?", "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embarcar aqueste estatut per o far veire sus un site Internet en copiar lo còdi çai-jos.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Semblarà aquò:",
"emoji_button.activity": "Activitats", "emoji_button.activity": "Activitats",
"emoji_button.flags": "Drapèus", "emoji_button.flags": "Drapèus",
"emoji_button.food": "Beure e manjar", "emoji_button.food": "Beure e manjar",
@ -164,7 +164,7 @@
"standalone.public_title": "Una ulhada dedins…", "standalone.public_title": "Una ulhada dedins…",
"status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat", "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
"status.delete": "Escafar", "status.delete": "Escafar",
"status.embed": "Embed", "status.embed": "Embarcar",
"status.favourite": "Apondre als favorits", "status.favourite": "Apondre als favorits",
"status.load_more": "Cargar mai", "status.load_more": "Cargar mai",
"status.media_hidden": "Mèdia rescondut", "status.media_hidden": "Mèdia rescondut",

View file

@ -1885,6 +1885,10 @@
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
&:last-child {
padding: 0 15px 0 0;
}
} }
.column-back-button__icon { .column-back-button__icon {
@ -2712,6 +2716,22 @@ button.icon-button.active i.fa-retweet {
} }
} }
.media-spoiler__video {
align-items: center;
background: $base-overlay-background;
color: $primary-text-color;
cursor: pointer;
display: flex;
flex-direction: column;
border: 0;
width: 100%;
height: 100%;
justify-content: center;
position: relative;
text-align: center;
z-index: 100;
}
.media-spoiler__warning { .media-spoiler__warning {
display: block; display: block;
font-size: 14px; font-size: 14px;
@ -4483,41 +4503,10 @@ noscript {
} }
} }
.embed-modal__html {
color: $ui-secondary-color;
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: none;
padding: 10px;
font-family: 'mastodon-font-monospace', monospace;
background: $ui-base-color;
color: $ui-primary-color;
font-size: 14px;
margin: 0;
margin-bottom: 15px;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.embed-modal { .embed-modal {
max-width: 80vw;
max-height: 80vh;
h4 { h4 {
padding: 30px; padding: 30px;
font-weight: 500; font-weight: 500;
@ -4525,18 +4514,52 @@ noscript {
text-align: center; text-align: center;
} }
.hint { .embed-modal__container {
margin-bottom: 15px; padding: 10px;
.hint {
margin-bottom: 15px;
}
.embed-modal__html {
color: $ui-secondary-color;
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: none;
padding: 10px;
font-family: 'mastodon-font-monospace', monospace;
background: $ui-base-color;
color: $ui-primary-color;
font-size: 14px;
margin: 0;
margin-bottom: 15px;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.embed-modal__iframe {
width: 400px;
max-width: 100%;
overflow: hidden;
border: 0;
}
} }
} }
.embed-modal__container {
padding: 10px;
}
.embed-modal__iframe {
width: 100%;
min-width: 400px;
overflow: hidden;
border: 0;
}

View file

@ -399,51 +399,54 @@
.embed { .embed {
.activity-stream { .activity-stream {
border-radius: 4px;
box-shadow: none; box-shadow: none;
.entry { .entry {
&:last-child {
border-radius: 0 0 4px 4px;
}
&:first-child { .detailed-status.light {
border-radius: 4px 4px 0 0; display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
&:last-child { .detailed-status__display-name {
border-radius: 4px; flex: 1;
margin: 0 5px 15px 0;
}
.button.button-secondary.logo-button {
flex: 0 auto;
font-size: 14px;
svg {
width: 20px;
height: auto;
vertical-align: middle;
margin-right: 5px;
path:first-child {
fill: $ui-primary-color;
}
path:last-child {
fill: $simple-background-color;
}
}
&:active,
&:focus,
&:hover {
svg path:first-child {
fill: lighten($ui-primary-color, 4%);
}
}
}
.status__content,
.detailed-status__meta {
flex: 100%;
} }
} }
} }
} }
} }
.button.button-secondary.logo-button {
position: absolute;
right: 14px;
top: 14px;
font-size: 14px;
svg {
width: 20px;
height: auto;
vertical-align: middle;
margin-right: 5px;
path:first-child {
fill: $ui-primary-color;
}
path:last-child {
fill: $simple-background-color;
}
}
&:active,
&:focus,
&:hover {
svg path:first-child {
fill: lighten($ui-primary-color, 4%);
}
}
}

View file

@ -33,7 +33,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def status_params def status_params
{ {
uri: @object['id'], uri: @object['id'],
url: @object['url'] || @object['id'], url: object_url || @object['id'],
account: @account, account: @account,
text: text_from_content || '', text: text_from_content || '',
language: language_from_content, language: language_from_content,
@ -147,6 +147,16 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@object['contentMap'].keys.first @object['contentMap'].keys.first
end end
def object_url
return if @object['url'].blank?
value = first_of_value(@object['url'])
return value if value.is_a?(String)
value['href']
end
def language_map? def language_map?
@object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty? @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
end end

View file

@ -65,7 +65,7 @@ class OStatus::AtomSerializer
add_namespaces(entry) if root add_namespaces(entry) if root
append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type)) append_element(entry, 'id', TagManager.instance.uri_for(stream_entry.status))
append_element(entry, 'published', stream_entry.created_at.iso8601) append_element(entry, 'published', stream_entry.created_at.iso8601)
append_element(entry, 'updated', stream_entry.updated_at.iso8601) append_element(entry, 'updated', stream_entry.updated_at.iso8601)
append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status") append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status")
@ -86,7 +86,7 @@ class OStatus::AtomSerializer
serialize_status_attributes(entry, stream_entry.status) serialize_status_attributes(entry, stream_entry.status)
end end
append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: account_stream_entry_url(stream_entry.account, stream_entry)) append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(stream_entry.status))
append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')) append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom'))
append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded? append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded?
append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil? append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil?

View file

@ -49,12 +49,17 @@ class TagManager
def unique_tag_to_local_id(tag, expected_type) def unique_tag_to_local_id(tag, expected_type)
return nil unless local_id?(tag) return nil unless local_id?(tag)
matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
return matches[1] unless matches.nil? if ActivityPub::TagManager.instance.local_uri?(tag)
ActivityPub::TagManager.instance.uri_to_local_id(tag)
else
matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
return matches[1] unless matches.nil?
end
end end
def local_id?(id) def local_id?(id)
id.start_with?("tag:#{Rails.configuration.x.local_domain}") id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id)
end end
def web_domain?(domain) def web_domain?(domain)
@ -92,7 +97,7 @@ class TagManager
when :person when :person
account_url(target) account_url(target)
when :note, :comment, :activity when :note, :comment, :activity
unique_tag(target.created_at, target.id, 'Status') target.uri || unique_tag(target.created_at, target.id, 'Status')
end end
end end

View file

@ -22,6 +22,7 @@
# reblogs_count :integer default(0), not null # reblogs_count :integer default(0), not null
# language :string # language :string
# conversation_id :integer # conversation_id :integer
# local :boolean
# #
class Status < ApplicationRecord class Status < ApplicationRecord
@ -84,7 +85,7 @@ class Status < ApplicationRecord
end end
def local? def local?
uri.nil? attributes['local'] || uri.nil?
end end
def reblog? def reblog?
@ -131,11 +132,14 @@ class Status < ApplicationRecord
!sensitive? && media_attachments.any? !sensitive? && media_attachments.any?
end end
after_create :store_uri, if: :local?
before_validation :prepare_contents, if: :local? before_validation :prepare_contents, if: :local?
before_validation :set_reblog before_validation :set_reblog
before_validation :set_visibility before_validation :set_visibility
before_validation :set_conversation before_validation :set_conversation
before_validation :set_sensitivity before_validation :set_sensitivity
before_validation :set_local
class << self class << self
def not_in_filtered_languages(account) def not_in_filtered_languages(account)
@ -253,6 +257,10 @@ class Status < ApplicationRecord
private private
def store_uri
update_attribute(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil?
end
def prepare_contents def prepare_contents
text&.strip! text&.strip!
spoiler_text&.strip! spoiler_text&.strip!
@ -292,4 +300,8 @@ class Status < ApplicationRecord
thread.account_id thread.account_id
end end
end end
def set_local
self.local = account.local?
end
end end

View file

@ -4,12 +4,12 @@ class AccountRelationshipsPresenter
attr_reader :following, :followed_by, :blocking, attr_reader :following, :followed_by, :blocking,
:muting, :requested, :domain_blocking :muting, :requested, :domain_blocking
def initialize(account_ids, current_account_id) def initialize(account_ids, current_account_id, options = {})
@following = Account.following_map(account_ids, current_account_id) @following = Account.following_map(account_ids, current_account_id).merge(options[:following_map] || {})
@followed_by = Account.followed_by_map(account_ids, current_account_id) @followed_by = Account.followed_by_map(account_ids, current_account_id).merge(options[:followed_by_map] || {})
@blocking = Account.blocking_map(account_ids, current_account_id) @blocking = Account.blocking_map(account_ids, current_account_id).merge(options[:blocking_map] || {})
@muting = Account.muting_map(account_ids, current_account_id) @muting = Account.muting_map(account_ids, current_account_id).merge(options[:muting_map] || {})
@requested = Account.requested_map(account_ids, current_account_id) @requested = Account.requested_map(account_ids, current_account_id).merge(options[:requested_map] || {})
@domain_blocking = Account.domain_blocking_map(account_ids, current_account_id) @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id).merge(options[:domain_blocking_map] || {})
end end
end end

View file

@ -4,7 +4,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
include RoutingHelper include RoutingHelper
attributes :id, :type, :following, :followers, attributes :id, :type, :following, :followers,
:inbox, :outbox, :shared_inbox, :inbox, :outbox,
:preferred_username, :name, :summary, :preferred_username, :name, :summary,
:url, :manually_approves_followers :url, :manually_approves_followers
@ -24,6 +24,18 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
end end
end end
class EndpointsSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :shared_inbox
def shared_inbox
inbox_url
end
end
has_one :endpoints, serializer: EndpointsSerializer
has_one :icon, serializer: ImageSerializer, if: :avatar_exists? has_one :icon, serializer: ImageSerializer, if: :avatar_exists?
has_one :image, serializer: ImageSerializer, if: :header_exists? has_one :image, serializer: ImageSerializer, if: :header_exists?
@ -51,8 +63,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
account_outbox_url(object) account_outbox_url(object)
end end
def shared_inbox def endpoints
inbox_url object
end end
def preferred_username def preferred_username

View file

@ -40,8 +40,7 @@ class OEmbedSerializer < ActiveModel::Serializer
attributes = { attributes = {
src: embed_short_account_status_url(object.account, object), src: embed_short_account_status_url(object.account, object),
class: 'mastodon-embed', class: 'mastodon-embed',
frameborder: '0', style: 'max-width: 100%; border: none;',
scrolling: 'no',
width: width, width: width,
height: height, height: height,
} }

View file

@ -6,7 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
# Should be called with confirmed valid JSON # Should be called with confirmed valid JSON
# and WebFinger-resolved username and domain # and WebFinger-resolved username and domain
def call(username, domain, json) def call(username, domain, json)
return unless json['inbox'].present? return if json['inbox'].blank?
@json = json @json = json
@uri = @json['id'] @uri = @json['id']
@ -42,9 +42,9 @@ class ActivityPub::ProcessAccountService < BaseService
@account.protocol = :activitypub @account.protocol = :activitypub
@account.inbox_url = @json['inbox'] || '' @account.inbox_url = @json['inbox'] || ''
@account.outbox_url = @json['outbox'] || '' @account.outbox_url = @json['outbox'] || ''
@account.shared_inbox_url = @json['sharedInbox'] || '' @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
@account.followers_url = @json['followers'] || '' @account.followers_url = @json['followers'] || ''
@account.url = @json['url'] || @uri @account.url = url || @uri
@account.display_name = @json['name'] || '' @account.display_name = @json['name'] || ''
@account.note = @json['summary'] || '' @account.note = @json['summary'] || ''
@account.avatar_remote_url = image_url('icon') @account.avatar_remote_url = image_url('icon')
@ -62,7 +62,7 @@ class ActivityPub::ProcessAccountService < BaseService
value = first_of_value(@json[key]) value = first_of_value(@json[key])
return if value.nil? return if value.nil?
return @json[key]['url'] if @json[key].is_a?(Hash) return value['url'] if value.is_a?(Hash)
image = fetch_resource(value) image = fetch_resource(value)
image['url'] if image image['url'] if image
@ -78,6 +78,16 @@ class ActivityPub::ProcessAccountService < BaseService
key['publicKeyPem'] if key key['publicKeyPem'] if key
end end
def url
return if @json['url'].blank?
value = first_of_value(@json['url'])
return value if value.is_a?(String)
value['href']
end
def auto_suspend? def auto_suspend?
domain_block && domain_block.suspend? domain_block && domain_block.suspend?
end end

View file

@ -41,7 +41,7 @@ class ProcessMentionsService < BaseService
NotifyService.new.call(mentioned_account, mention) NotifyService.new.call(mentioned_account, mention)
elsif mentioned_account.ostatus? && (Rails.configuration.x.use_ostatus_privacy || !status.stream_entry.hidden?) elsif mentioned_account.ostatus? && (Rails.configuration.x.use_ostatus_privacy || !status.stream_entry.hidden?)
NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id) NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
elsif mentioned_account.activitypub? && !mentioned_account.following?(status.account) elsif mentioned_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(mention.status), mention.status.account_id, mentioned_account.inbox_url) ActivityPub::DeliveryWorker.perform_async(build_json(mention.status), mention.status.account_id, mentioned_account.inbox_url)
end end
end end

View file

@ -21,13 +21,13 @@
= stylesheet_pack_tag 'common', media: 'all' = stylesheet_pack_tag 'common', media: 'all'
= javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
= javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/getting_started.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/compose.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/home_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/notifications.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/community_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/public_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'emojione_picker', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('emojione_picker.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
= csrf_meta_tags = csrf_meta_tags

View file

@ -1,9 +1,4 @@
.detailed-status.light .detailed-status.light
- if embedded_view?
= link_to "web+mastodon://follow?uri=#{status.account.local_username_and_domain}", class: 'button button-secondary logo-button', target: '_new' do
= render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')
= t('accounts.follow')
= link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do
%div %div
.avatar .avatar
@ -12,6 +7,11 @@
%strong.p-name.emojify= display_name(status.account) %strong.p-name.emojify= display_name(status.account)
%span= acct(status.account) %span= acct(status.account)
- if embedded_view?
= link_to "web+mastodon://follow?uri=#{status.account.local_username_and_domain}", class: 'button button-secondary logo-button', target: '_new' do
= render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')
= t('accounts.follow')
.status__content.p-name.emojify< .status__content.p-name.emojify<
- if status.spoiler_text? - if status.spoiler_text?
%p{ style: 'margin-bottom: 0' }< %p{ style: 'margin-bottom: 0' }<

View file

@ -6,7 +6,7 @@ class Pubsubhubbub::DistributionWorker
sidekiq_options queue: 'push' sidekiq_options queue: 'push'
def perform(stream_entry_ids) def perform(stream_entry_ids)
stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status&.direct_visibility? } stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status.nil? || e.status.direct_visibility? }
return if stream_entries.empty? return if stream_entries.empty?

View file

@ -3,7 +3,7 @@
class Pubsubhubbub::SubscribeWorker class Pubsubhubbub::SubscribeWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false, unique_retry: true
sidekiq_retry_in do |count| sidekiq_retry_in do |count|
case count case count

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class ResolveRemoteAccountWorker
include Sidekiq::Worker
sidekiq_options queue: 'pull', unique: :until_executed
def perform(uri)
ResolveRemoteAccountService.new.call(uri)
end
end

View file

@ -10,6 +10,7 @@ require_relative '../app/lib/exceptions'
require_relative '../lib/paperclip/gif_transcoder' require_relative '../lib/paperclip/gif_transcoder'
require_relative '../lib/paperclip/video_transcoder' require_relative '../lib/paperclip/video_transcoder'
require_relative '../lib/mastodon/version' require_relative '../lib/mastodon/version'
require_relative '../lib/mastodon/unique_retry_job_middleware'
Dotenv::Railtie.load Dotenv::Railtie.load

View file

@ -12,6 +12,7 @@ Rails.application.configure do
config.x.web_domain = web_host config.x.web_domain = web_host
config.x.use_https = https config.x.use_https = https
config.x.use_s3 = ENV['S3_ENABLED'] == 'true' config.x.use_s3 = ENV['S3_ENABLED'] == 'true'
config.x.use_swift = ENV['SWIFT_ENABLED'] == 'true'
config.x.alternate_domains = alternate_domains.split(/\s*,\s*/) config.x.alternate_domains = alternate_domains.split(/\s*,\s*/)

View file

@ -40,6 +40,21 @@ if ENV['S3_ENABLED'] == 'true'
Paperclip::Attachment.default_options[:url] = ':s3_alias_url' Paperclip::Attachment.default_options[:url] = ':s3_alias_url'
Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST'] Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST']
end end
elsif ENV['SWIFT_ENABLED'] == 'true'
Paperclip::Attachment.default_options.merge!(
path: ':class/:attachment/:id_partition/:style/:filename',
storage: :fog,
fog_credentials: {
provider: 'OpenStack',
openstack_username: ENV.fetch('SWIFT_USERNAME'),
openstack_tenant: ENV.fetch('SWIFT_TENANT'),
openstack_api_key: ENV.fetch('SWIFT_PASSWORD'),
openstack_auth_url: ENV.fetch('SWIFT_AUTH_URL'),
},
fog_directory: ENV.fetch('SWIFT_CONTAINER'),
fog_host: ENV.fetch('SWIFT_OBJECT_URL'),
fog_public: true
)
else else
Paperclip::Attachment.default_options[:path] = (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename' Paperclip::Attachment.default_options[:path] = (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename'
Paperclip::Attachment.default_options[:url] = (ENV['PAPERCLIP_ROOT_URL'] || '/system') + '/:class/:attachment/:id_partition/:style/:filename' Paperclip::Attachment.default_options[:url] = (ENV['PAPERCLIP_ROOT_URL'] || '/system') + '/:class/:attachment/:id_partition/:style/:filename'

View file

@ -13,4 +13,7 @@ end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|
config.redis = redis_params config.redis = redis_params
config.client_middleware do |chain|
chain.add Mastodon::UniqueRetryJobMiddleware
end
end end

View file

@ -47,16 +47,16 @@ ar:
datetime: datetime:
distance_in_words: distance_in_words:
about_x_hours: "%{count}سا" about_x_hours: "%{count}سا"
about_x_months: "%{count}شهر" about_x_months: "%{count} شهر"
about_x_years: "%{count}سنة" about_x_years: "%{count} سنة"
almost_x_years: "%{count}سنوات" almost_x_years: "%{count} سنوات"
half_a_minute: Just now half_a_minute: الآن
less_than_x_minutes: "%{count}د" less_than_x_minutes: "%{count} د"
less_than_x_seconds: الآن less_than_x_seconds: الآن
over_x_years: "%{count}سنين" over_x_years: "%{count} سنين"
x_days: "%{count}أيام" x_days: "%{count} أيام"
x_minutes: "%{count}د" x_minutes: "%{count}د"
x_months: "%{count}شه" x_months: "%{count} شه"
x_seconds: "%{count}ث" x_seconds: "%{count}ث"
exports: exports:
blocks: قمت بحظر blocks: قمت بحظر
@ -94,7 +94,7 @@ ar:
one: "إشعار واحد منذ زيارتك الأخيرة \U0001F418" one: "إشعار واحد منذ زيارتك الأخيرة \U0001F418"
other: "%{count} إشعارات جديدة منذ زيارتك الأخيرة \U0001F418" other: "%{count} إشعارات جديدة منذ زيارتك الأخيرة \U0001F418"
favourite: favourite:
body: 'Your status was favourited by %{name}:' body: 'أُعجب %{name} بمنشورك'
subject: "%{name} favourited your status" subject: "%{name} favourited your status"
follow: follow:
body: "%{name} من متتبعيك الآن !" body: "%{name} من متتبعيك الآن !"
@ -108,6 +108,17 @@ ar:
reblog: reblog:
body: 'Your status was boosted by %{name}:' body: 'Your status was boosted by %{name}:'
subject: "%{name} boosted your status" subject: "%{name} boosted your status"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: التالي next: التالي
prev: السابق prev: السابق
@ -148,7 +159,7 @@ ar:
enabled_success: تم تفعيل إثبات الهوية المزدوج بنجاح enabled_success: تم تفعيل إثبات الهوية المزدوج بنجاح
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in." instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:' manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
setup: Set up setup: تنشيط
wrong_code: الرمز الذي أدخلته غير صالح. تحقق من صحة الوقت على الخادم و الجهاز. wrong_code: الرمز الذي أدخلته غير صالح. تحقق من صحة الوقت على الخادم و الجهاز.
users: users:
invalid_email: عنوان البريد الإلكتروني غير صالح invalid_email: عنوان البريد الإلكتروني غير صالح

View file

@ -108,6 +108,17 @@ bg:
reblog: reblog:
body: 'Твоята публикация беше споделена от %{name}:' body: 'Твоята публикация беше споделена от %{name}:'
subject: "%{name} сподели публикацията ти" subject: "%{name} сподели публикацията ти"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Напред next: Напред
prev: Назад prev: Назад

View file

@ -340,6 +340,17 @@ ca:
reblog: reblog:
body: "%{name} ha retootejat el teu estat" body: "%{name} ha retootejat el teu estat"
subject: "%{name} ha retootejat el teu estat" subject: "%{name} ha retootejat el teu estat"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Pròxim next: Pròxim
prev: Anterior prev: Anterior

View file

@ -12,15 +12,15 @@ de:
source_code: Quellcode source_code: Quellcode
status_count_after: Beiträge verfassten status_count_after: Beiträge verfassten
status_count_before: die status_count_before: die
user_count_after: Benutzer user_count_after: Profile
user_count_before: Heimat für user_count_before: Heimat für
accounts: accounts:
follow: Folgen follow: Folgen
followers: Folgende followers: Folgende
following: Folgt following: Folgt
nothing_here: Hier gibt es nichts! nothing_here: Hier gibt es nichts!
people_followed_by: Nutzer, denen %{name} folgt people_followed_by: Profile, denen %{name} folgt
people_who_follow: Nutzer, die %{name} folgen people_who_follow: Profile, die %{name} folgen
posts: Beiträge posts: Beiträge
remote_follow: Folgen remote_follow: Folgen
unfollow: Entfolgen unfollow: Entfolgen
@ -67,7 +67,7 @@ de:
title: Konten title: Konten
undo_silenced: Stummschaltung zurücknehmen undo_silenced: Stummschaltung zurücknehmen
undo_suspension: Sperre zurücknehmen undo_suspension: Sperre zurücknehmen
username: Benutzername username: Profilname
web: Web web: Web
domain_blocks: domain_blocks:
add_new: Neu hinzufügen add_new: Neu hinzufügen
@ -124,7 +124,7 @@ de:
settings: settings:
contact_information: contact_information:
email: Eine öffentliche E-Mail-Adresse angeben email: Eine öffentliche E-Mail-Adresse angeben
username: Einen Benutzernamen angeben username: Einen Profilnamen angeben
registrations: registrations:
closed_message: closed_message:
desc_html: Wird auf der Frontseite angezeigt, wenn die Registrierung geschlossen ist<br>Du kannst HTML-Tags benutzen desc_html: Wird auf der Frontseite angezeigt, wenn die Registrierung geschlossen ist<br>Du kannst HTML-Tags benutzen
@ -208,7 +208,7 @@ de:
following: Folgeliste following: Folgeliste
muting: Stummschaltungsliste muting: Stummschaltungsliste
upload: Hochladen upload: Hochladen
landing_strip_html: "<strong>%{name}</strong> ist ein Benutzer auf %{link_to_root_path}. Du kannst ihm folgen oder mit ihm interagieren, sofern du ein Konto irgendwo in der Fediverse hast." landing_strip_html: "<strong>%{name}</strong> hat ein Profil auf %{link_to_root_path}. Du kannst folgen oder interagieren, sofern du ein Konto irgendwo im Fediversum hast."
landing_strip_signup_html: Wenn nicht, kannst du dich <a href="%{sign_up_path}">hier anmelden</a>. landing_strip_signup_html: Wenn nicht, kannst du dich <a href="%{sign_up_path}">hier anmelden</a>.
media_attachments: media_attachments:
validations: validations:
@ -239,12 +239,23 @@ de:
reblog: reblog:
body: 'Dein Beitrag wurde von %{name} geteilt:' body: 'Dein Beitrag wurde von %{name} geteilt:'
subject: "%{name} teilte deinen Beitrag." subject: "%{name} teilte deinen Beitrag."
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Vorwärts next: Vorwärts
prev: Zurück prev: Zurück
truncate: "&hellip;" truncate: "&hellip;"
remote_follow: remote_follow:
acct: Dein Nutzername@Domain, von dem aus du dieser Person folgen möchtest. acct: Dein Profilname@Domain, von dem aus du dieser Person folgen möchtest.
missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden. missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden.
proceed: Weiter proceed: Weiter
prompt: 'Du wirst dieser Person folgen:' prompt: 'Du wirst dieser Person folgen:'

View file

@ -77,7 +77,7 @@ de:
invalid_grant: Die bereitgestellte Autorisierung ist inkorrekt, abgelaufen, widerrufen, ist mit einem anderen Client verknüpft oder der Redirection URI stimmt nicht mit der Autorisierungs-Anfrage überein. invalid_grant: Die bereitgestellte Autorisierung ist inkorrekt, abgelaufen, widerrufen, ist mit einem anderen Client verknüpft oder der Redirection URI stimmt nicht mit der Autorisierungs-Anfrage überein.
invalid_redirect_uri: Der Redirect-URI in der Anfrage ist ungültig. invalid_redirect_uri: Der Redirect-URI in der Anfrage ist ungültig.
invalid_request: Die Anfrage enthält einen nicht-unterstützten Parameter, ein Parameter fehlt oder sie ist anderweitig fehlerhaft. invalid_request: Die Anfrage enthält einen nicht-unterstützten Parameter, ein Parameter fehlt oder sie ist anderweitig fehlerhaft.
invalid_resource_owner: Die angegebenen Zugangsdaten für den "Resource Owner" sind inkorrekt oder dieser Benutzer existiert nicht. invalid_resource_owner: Die angegebenen Zugangsdaten für den "Resource Owner" sind inkorrekt oder dieses Profil existiert nicht.
invalid_scope: Der angeforderte Scope ist inkorrekt, unbekannt oder fehlerhaft. invalid_scope: Der angeforderte Scope ist inkorrekt, unbekannt oder fehlerhaft.
invalid_token: invalid_token:
expired: Der Zugriffstoken ist abgelaufen expired: Der Zugriffstoken ist abgelaufen
@ -108,6 +108,6 @@ de:
application: application:
title: OAuth-Autorisierung nötig title: OAuth-Autorisierung nötig
scopes: scopes:
follow: Nutzer folgen, blocken, entblocken und entfolgen follow: Profil folgen, blocken, entblocken und entfolgen
read: deine Daten lesen read: deine Daten lesen
write: Beiträge von deinem Konto aus veröffentlichen write: Beiträge von deinem Konto aus veröffentlichen

View file

@ -3,8 +3,10 @@ fa:
activerecord: activerecord:
attributes: attributes:
doorkeeper/application: doorkeeper/application:
name: Name name: Application name
redirect_uri: Redirect URI redirect_uri: Redirect URI
scopes: Scopes
website: Application website
errors: errors:
models: models:
doorkeeper/application: doorkeeper/application:
@ -33,18 +35,22 @@ fa:
redirect_uri: Use one line per URI redirect_uri: Use one line per URI
scopes: Separate scopes with spaces. Leave blank to use the default scopes. scopes: Separate scopes with spaces. Leave blank to use the default scopes.
index: index:
application: Application
callback_url: Callback URL callback_url: Callback URL
delete: Delete
name: Name name: Name
new: New Application new: New application
scopes: Scopes
show: Show
title: Your applications title: Your applications
new: new:
title: New Application title: New application
show: show:
actions: Actions actions: Actions
application_id: Application Id application_id: Client key
callback_urls: Callback urls callback_urls: Callback URLs
scopes: Scopes scopes: Scopes
secret: Secret secret: Client secret
title: 'Application: %{name}' title: 'Application: %{name}'
authorizations: authorizations:
buttons: buttons:

View file

@ -5,6 +5,8 @@ oc:
doorkeeper/application: doorkeeper/application:
name: Nom name: Nom
redirect_uri: URL de redireccion redirect_uri: URL de redireccion
scopes: Encastres
website: Aplicacion web
errors: errors:
models: models:
doorkeeper/application: doorkeeper/application:
@ -33,9 +35,13 @@ oc:
redirect_uri: Utilizatz una linha per URI redirect_uri: Utilizatz una linha per URI
scopes: Separatz los encastres amb despacis. Daissatz void per utilizar lencastre per defaut. scopes: Separatz los encastres amb despacis. Daissatz void per utilizar lencastre per defaut.
index: index:
application: Aplicacion
callback_url: URL de rapèl callback_url: URL de rapèl
delete: Suprimir
name: Nom name: Nom
new: Nòva aplicacion new: Nòva aplicacion
scopes: Encastres
show: Veire
title: Vòstras aplicacions title: Vòstras aplicacions
new: new:
title: Nòva aplicacion title: Nòva aplicacion

View file

@ -103,6 +103,17 @@ eo:
reblog: reblog:
body: "%{name} diskonigis vian mesaĝon:" body: "%{name} diskonigis vian mesaĝon:"
subject: "%{name} diskonigis vian mesaĝon" subject: "%{name} diskonigis vian mesaĝon"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Sekva next: Sekva
prev: Malsekva prev: Malsekva

View file

@ -108,6 +108,17 @@ es:
reblog: reblog:
body: "%{name} ha retooteado tu estado" body: "%{name} ha retooteado tu estado"
subject: "%{name} ha retooteado tu estado" subject: "%{name} ha retooteado tu estado"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Próximo next: Próximo
prev: Anterior prev: Anterior

View file

@ -339,6 +339,17 @@ fa:
reblog: reblog:
body: "%{name} نوشتهٔ شما را بازبوقید:" body: "%{name} نوشتهٔ شما را بازبوقید:"
subject: "%{name} نوشتهٔ شما را بازبوقید" subject: "%{name} نوشتهٔ شما را بازبوقید"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: بعدی next: بعدی
prev: قبلی prev: قبلی

View file

@ -103,6 +103,17 @@ fi:
reblog: reblog:
body: 'Sinun statustasi boostasi %{name}:' body: 'Sinun statustasi boostasi %{name}:'
subject: "%{name} boostasi statustasi" subject: "%{name} boostasi statustasi"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Seuraava next: Seuraava
prev: Edellinen prev: Edellinen

View file

@ -358,6 +358,17 @@ fr:
reblog: reblog:
body: "%{name} a partagé votre statut:" body: "%{name} a partagé votre statut:"
subject: "%{name} a partagé votre statut" subject: "%{name} a partagé votre statut"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Suivant next: Suivant
prev: Précédent prev: Précédent

View file

@ -264,6 +264,17 @@ he:
reblog: reblog:
body: 'חצרוצך הודהד על ידי %{name}:' body: 'חצרוצך הודהד על ידי %{name}:'
subject: חצרוצך הודהד על ידי%{name} subject: חצרוצך הודהד על ידי%{name}
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: הבא next: הבא
prev: הקודם prev: הקודם

View file

@ -105,6 +105,17 @@ hr:
reblog: reblog:
body: 'Tvoj status je potaknut od %{name}:' body: 'Tvoj status je potaknut od %{name}:'
subject: "%{name} je potakao tvoj status" subject: "%{name} je potakao tvoj status"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Sljedeći next: Sljedeći
prev: Prošli prev: Prošli

View file

@ -45,6 +45,17 @@ hu:
reblog: reblog:
body: 'Az állapotod reblogolta %{name}:' body: 'Az állapotod reblogolta %{name}:'
subject: "%{name} reblogolta az állapotod" subject: "%{name} reblogolta az állapotod"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Következő next: Következő
prev: Előző prev: Előző

View file

@ -254,6 +254,17 @@ id:
reblog: reblog:
body: 'Status anda di-boost oleh %{name}:' body: 'Status anda di-boost oleh %{name}:'
subject: "%{name} mem-boost status anda" subject: "%{name} mem-boost status anda"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Selanjutnya next: Selanjutnya
prev: Sebelumnya prev: Sebelumnya

View file

@ -239,6 +239,17 @@ io:
reblog: reblog:
body: "%{name} diskonocigis tua mesajo:" body: "%{name} diskonocigis tua mesajo:"
subject: "%{name} diskonocigis tua mesajo" subject: "%{name} diskonocigis tua mesajo"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Sequanta next: Sequanta
prev: Preiranta prev: Preiranta

View file

@ -108,6 +108,17 @@ it:
reblog: reblog:
body: 'Il tuo status è stato condiviso da %{name}:' body: 'Il tuo status è stato condiviso da %{name}:'
subject: "%{name} ha condiviso il tuo status" subject: "%{name} ha condiviso il tuo status"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Avanti next: Avanti
prev: Indietro prev: Indietro

View file

@ -1,29 +1,52 @@
--- ---
ko: ko:
about: about:
about_mastodon_html: Mastodon 은<em>자유로운 오픈 소스</em>소셜 네트워크입니다. 상용 플랫폼의 대체로써 <em>분산형 구조</em>를 채택해, 여러분의 대화가 한 회사에 독점되는 것을 방지합니다. 신뢰할 수 있는 인스턴스를 선택하세요 &mdash; 어떤 인스턴스를 고르더라도, 누구와도 대화할 수 있습니다. 누구나 자신만의 Mastodon 인스턴스를 만들 수 있으며, Seamless하게 <em>소셜 네트워크</em>에 참가할 수 있습니다. about_mastodon_html: Mastodon은 <em>오픈 소스 기반의</em> 소셜 네트워크 서비스 입니다. 상용 플랫폼의 대체로서 <em>분산형 구조</em>를 채택해, 여러분의 대화가 한 회사에 독점되는 것을 방지합니다. 신뢰할 수 있는 인스턴스를 선택하세요 &mdash; 어떤 인스턴스를 고르더라도, 누구와도 대화할 수 있습니다. 누구나 자신만의 Mastodon 인스턴스를 만들 수 있으며, Seamless하게 <em>소셜 네트워크</em>에 참가할 수 있습니다.
about_this: 이 인스턴스에 대해서 about_this: 이 인스턴스에 대해서
closed_registrations: 현재 이 인스턴스에서는 신규 등록을 받고 있지 않습니다. closed_registrations: 현재 이 인스턴스에서는 신규 등록을 받고 있지 않습니다.
contact: 연락처 contact: 연락처
description_headline: "%{domain} 는 무엇인가요?" contact_missing: 미설정
contact_unavailable: N/A
description_headline: "%{domain} (은)는 무엇인가요?"
domain_count_after: 개의 인스턴스 domain_count_after: 개의 인스턴스
domain_count_before: 연결됨 domain_count_before: 연결된
extended_description_html: |
<h3>룰을 작성하는 장소</h3>
<p>아직 설명이 작성되지 않았습니다.</p>
features:
humane_approach_body: 다른 SNS의 실패를 교훈삼아, Mastodon은 소셜미디어가 잘못 사용되는 것을 막기 위하여 윤리적인 설계를 추구합니다.
humane_approach_title: 보다 배려를 의식한 설계를 추구
not_a_product_body: Mastodon은 이익을 추구하는 SNS가 아닙니다. 그러므로 광고와 데이터의 수집 및 분석이 존재하지 않고, 유저를 구속하지도 않습니다.
not_a_product_title: 여러분은 사람이며, 상품이 아닙니다.
real_conversation_body: 자유롭게 사용할 수 있는 500문자의 메세지와 미디어 경고 내용을 바탕으로, 자기자신을 자유롭게 표현할 수 있습니다.
real_conversation_title: 진정한 커뮤니케이션을 위하여
within_reach_body: 개발자 친화적인 API에 의해서 실현된 iOS나 Android, 그 외의 여러 Platform들 덕분에 어디서든 친구들과 자유롭게 메세지를 주고 받을 수 있습니다.
within_reach_title: 언제나 유저의 곁에서
find_another_instance: 다른 인스턴스 찾기
generic_description: "%{domain} 은 Mastodon의 인스턴스 입니다."
hosted_on: Mastodon hosted on %{domain}
learn_more: 자세히
other_instances: 다른 인스턴스 other_instances: 다른 인스턴스
source_code: 소스 코드 source_code: 소스 코드
status_count_after: Toot status_count_after: Toot
status_count_before: Toot 수 status_count_before: Toot 수
user_count_after: user_count_after:
user_count_before: 사용자 수 user_count_before: 사용자 수
what_is_mastodon: Mastodon이란?
accounts: accounts:
follow: 팔로우 follow: 팔로우
followers: 팔로워 followers: 팔로워
following: 팔로잉 following: 팔로잉
media: 미디어
nothing_here: 아무 것도 없습니다. nothing_here: 아무 것도 없습니다.
people_followed_by: "%{name} 님이 팔로우 중인 계정" people_followed_by: "%{name} 님이 팔로우 중인 계정"
people_who_follow: "%{name} 님을 팔로우 중인 계정" people_who_follow: "%{name} 님을 팔로우 중인 계정"
posts: 포스트 posts: Toot
posts_with_replies: Toot와 답장
remote_follow: 리모트 팔로우 remote_follow: 리모트 팔로우
reserved_username: 이 아이디는 예약되어 있습니다. reserved_username: 이 아이디는 예약되어 있습니다.
roles:
admin: Admin
unfollow: 팔로우 해제 unfollow: 팔로우 해제
admin: admin:
accounts: accounts:
@ -38,6 +61,7 @@ ko:
feed_url: 피드 URL feed_url: 피드 URL
followers: 팔로워 수 followers: 팔로워 수
follows: 팔로잉 수 follows: 팔로잉 수
inbox_url: Inbox URL
ip: IP ip: IP
location: location:
all: 전체 all: 전체
@ -57,8 +81,10 @@ ko:
alphabetic: 알파벳 순 alphabetic: 알파벳 순
most_recent: 최근 활동 순 most_recent: 최근 활동 순
title: 순서 title: 순서
outbox_url: Outbox URL
perform_full_suspension: 완전히 정지시키기 perform_full_suspension: 완전히 정지시키기
profile_url: 프로필 URL profile_url: 프로필 URL
protocol: Protocol
public: 전체 공개 public: 전체 공개
push_subscription_expires: PuSH 구독 기간 만료 push_subscription_expires: PuSH 구독 기간 만료
redownload: 아바타 업데이트 redownload: 아바타 업데이트
@ -90,12 +116,14 @@ ko:
hint: 도메인 차단은 내부 데이터베이스에 계정이 생성되는 것까지는 막을 수 없지만, 그 도메인에서 생성된 계정에 자동적으로 특정한 모더레이션을 적용하게 할 수 있습니다. hint: 도메인 차단은 내부 데이터베이스에 계정이 생성되는 것까지는 막을 수 없지만, 그 도메인에서 생성된 계정에 자동적으로 특정한 모더레이션을 적용하게 할 수 있습니다.
severity: severity:
desc_html: "<strong>침묵</strong>은 계정을 팔로우 하지 않고 있는 사람들에겐 계정의 Toot을 보이지 않게 합니다. <strong>정지</strong>는 계정의 컨텐츠, 미디어, 프로필 데이터를 삭제합니다." desc_html: "<strong>침묵</strong>은 계정을 팔로우 하지 않고 있는 사람들에겐 계정의 Toot을 보이지 않게 합니다. <strong>정지</strong>는 계정의 컨텐츠, 미디어, 프로필 데이터를 삭제합니다."
noop: 없음
silence: 침묵 silence: 침묵
suspend: 정지 suspend: 정지
title: 새로운 도메인 차단 title: 새로운 도메인 차단
reject_media: 미디어 파일 거부하기 reject_media: 미디어 파일 거부하기
reject_media_hint: 로컬에 저장된 미디어 파일을 삭제하고, 이후로도 다운로드를 거부합니다. 정지하고는 관계 없습니다. reject_media_hint: 로컬에 저장된 미디어 파일을 삭제하고, 이후로도 다운로드를 거부합니다. 정지하고는 관계 없습니다.
severities: severities:
noop: 없음
silence: 침묵 silence: 침묵
suspend: 정지 suspend: 정지
severity: 심각도 severity: 심각도
@ -146,16 +174,41 @@ ko:
closed_message: closed_message:
desc_html: 신규 등록을 받지 않을 때 프론트 페이지에 표시됩니다. <br>HTML 태그를 사용할 수 있습니다. desc_html: 신규 등록을 받지 않을 때 프론트 페이지에 표시됩니다. <br>HTML 태그를 사용할 수 있습니다.
title: 신규 등록 정지 시 메시지 title: 신규 등록 정지 시 메시지
deletion:
desc_html: 유저가 자신의 계정을 삭제할 수 있도록 설정합니다.
title: 계정 삭제를 허가함
open: open:
title: 신규 등록을 받음 desc_html: 유저가 자신의 계정을 생성할 수 있도록 설정합니다.
title: 신규 계정 등록을 받음
site_description: site_description:
desc_html: 탑 페이지와 meta 태그에 사용됩니다.<br>HTML 태그, 예를 들어<code>&lt;a&gt;</code> 태그와 <code>&lt;em&gt;</code> 태그를 사용할 수 있습니다. desc_html: 탑 페이지와 meta 태그에 사용됩니다.<br>HTML 태그, 예를 들어<code>&lt;a&gt;</code> 태그와 <code>&lt;em&gt;</code> 태그를 사용할 수 있습니다.
title: 사이트 설명 title: 사이트 설명
site_description_extended: site_description_extended:
desc_html: 인스턴스 정보 페이지에 표시됩니다.<br>HTML 태그를 사용할 수 있습니다. desc_html: 인스턴스 정보 페이지에 표시됩니다.<br>HTML 태그를 사용할 수 있습니다.
title: 사이트 상세 설명 title: 사이트 상세 설명
site_terms:
desc_html: 당신은 독자적인 개인정보 취급 방침이나 이용약관, 그 외의 법적 근거를 작성할 수 있습니다. 또한 HTML태그를 사용할 수 있습니다.
title: 커스텀 서비스 이용 약관
site_title: 사이트 이름 site_title: 사이트 이름
timeline_preview:
desc_html: Landing page에 공개 타임라인을 표시합니다.
title: 타임라인 프리뷰
title: 사이트 설정 title: 사이트 설정
statuses:
back_to_account: 계정으로 돌아가기
batch:
delete: 삭제
nsfw_off: NSFW 끄기
nsfw_on: NSFW 켜기
execute: 실행
failed_to_execute: 실행이 실패하였습니다.
media:
hide: 미디어 숨기기
show: 미디어 보여주기
title: 미디어
no_media: 미디어 없음
title: 계정 Toot
with_media: 미디어 있음
subscriptions: subscriptions:
callback_url: 콜백 URL callback_url: 콜백 URL
confirmed: 확인됨 confirmed: 확인됨
@ -173,13 +226,21 @@ ko:
signature: Mastodon %{instance} 인스턴스로에서 알림 signature: Mastodon %{instance} 인스턴스로에서 알림
view: 'View:' view: 'View:'
applications: applications:
created: 어플리케이션이 작성되었습니다.
destroyed: 어플리케이션이 삭제되었습니다.
invalid_url: 올바르지 않은 URL입니다 invalid_url: 올바르지 않은 URL입니다
regenerate_token: 토큰 재생성
token_regenerated: 액세스 토큰이 재생성되었습니다.
warning: 이 데이터는 다른 사람들과 절대로 공유하지 마세요.
your_token: 액세스 토큰
auth: auth:
agreement_html: 이 등록으로 <a href="%{rules_path}">이용규약</a> 과 <a href="%{terms_path}">개인정보 취급 방침</a>에 동의하는 것으로 간주됩니다.
change_password: 보안 change_password: 보안
delete_account: 계정 삭제 delete_account: 계정 삭제
delete_account_html: 계정을 삭제하고 싶은 경우, <a href="%{path}">여기서</a> 삭제할 수 있습니다. 삭제 전 확인 화면이 표시됩니다. delete_account_html: 계정을 삭제하고 싶은 경우, <a href="%{path}">여기서</a> 삭제할 수 있습니다. 삭제 전 확인 화면이 표시됩니다.
didnt_get_confirmation: 확인 메일을 받지 못하셨습니까? didnt_get_confirmation: 확인 메일을 받지 못하셨습니까?
forgot_password: 비밀번호를 잊어버리셨습니까? forgot_password: 비밀번호를 잊어버리셨습니까?
invalid_reset_password_token: 비밀번호 리셋 토큰이 올바르지 못하거나 기간이 만료되었습니다. 다시 요청해주세요.
login: 로그인 login: 로그인
logout: 로그아웃 logout: 로그아웃
register: 등록하기 register: 등록하기
@ -189,6 +250,12 @@ ko:
authorize_follow: authorize_follow:
error: 리모트 팔로우 도중 오류가 발생했습니다. error: 리모트 팔로우 도중 오류가 발생했습니다.
follow: 팔로우 follow: 팔로우
follow_request: '당신은 다음 계정에 팔로우 신청을 했습니다:'
following: '성공! 당신은 다음 계정을 팔로우 하고 있습니다:'
post_follow:
close: 혹은, 당신은 이 윈도우를 닫을 수 있습니다
return: 유저 프로필로 돌아가기
web: 웹으로 가기
title: "%{acct} 를 팔로우" title: "%{acct} 를 팔로우"
datetime: datetime:
distance_in_words: distance_in_words:
@ -271,8 +338,8 @@ ko:
one: "1건의 새로운 알림 \U0001F418" one: "1건의 새로운 알림 \U0001F418"
other: "%{count}건의 새로운 알림 \U0001F418" other: "%{count}건의 새로운 알림 \U0001F418"
favourite: favourite:
body: "%{name} 님이 내 Toot 즐겨찾기에 등록했습니다." body: "%{name} 님이 내 Toot 즐겨찾기에 등록했습니다."
subject: "%{name} 님이 내 Toot 즐겨찾기에 등록했습니다" subject: "%{name} 님이 내 Toot 즐겨찾기에 등록했습니다"
follow: follow:
body: "%{name} 님이 나를 팔로우 했습니다" body: "%{name} 님이 나를 팔로우 했습니다"
subject: "%{name} 님이 나를 팔로우 했습니다" subject: "%{name} 님이 나를 팔로우 했습니다"
@ -285,10 +352,35 @@ ko:
reblog: reblog:
body: "%{name} 님이 내 Toot을 부스트 했습니다:" body: "%{name} 님이 내 Toot을 부스트 했습니다:"
subject: "%{name} 님이 내 Toot을 부스트 했습니다" subject: "%{name} 님이 내 Toot을 부스트 했습니다"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: 다음 next: 다음
prev: 이전 prev: 이전
truncate: "&hellip;" truncate: "&hellip;"
push_notifications:
favourite:
title: "%{name} 님이 당신의 Toot를 즐겨찾기에 등록했습니다."
follow:
title: "%{name} 님이 나를 팔로우 하고 있습니다."
group:
title: "%{count} 건의 알림"
mention:
action_boost: 부스트
action_expand: 더보기
action_favourite: 즐겨찾기
title: "%{name} 님이 답장을 보냈습니다"
reblog:
title: "%{name} 님이 당신의 Toot를 부스트 했습니다."
remote_follow: remote_follow:
acct: 아이디@도메인을 입력해 주십시오 acct: 아이디@도메인을 입력해 주십시오
missing_resource: 리디렉션 대상을 찾을 수 없습니다 missing_resource: 리디렉션 대상을 찾을 수 없습니다
@ -330,11 +422,14 @@ ko:
windows: Windows windows: Windows
windows_mobile: Windows Mobile windows_mobile: Windows Mobile
windows_phone: Windows Phone windows_phone: Windows Phone
revoke: 삭제
revoke_success: 세션이 삭제되었습니다.
title: 세션 title: 세션
settings: settings:
authorized_apps: 인증된 어플리케이션 authorized_apps: 인증된 어플리케이션
back: 돌아가기 back: 돌아가기
delete: 계정 삭제 delete: 계정 삭제
development: 개발
edit_profile: 프로필 편집 edit_profile: 프로필 편집
export: 데이터 내보내기 export: 데이터 내보내기
followers: 신뢰 중인 인스턴스 followers: 신뢰 중인 인스턴스
@ -342,9 +437,14 @@ ko:
preferences: 사용자 설정 preferences: 사용자 설정
settings: 설정 settings: 설정
two_factor_authentication: 2단계 인증 two_factor_authentication: 2단계 인증
your_apps: 애플리케이션
statuses: statuses:
open_in_web: Web으로 열기 open_in_web: Web으로 열기
over_character_limit: 최대 %{max}자까지 입력할 수 있습니다 over_character_limit: 최대 %{max}자까지 입력할 수 있습니다
pin_errors:
ownership: 다른 사람의 Toot는 고정될 수 없습니다.
private: 비공개 Toot는 고정될 수 없습니다.
reblog: 부스트는 고정될 수 없습니다.
show_more: 더 보기 show_more: 더 보기
visibilities: visibilities:
private: 비공개 private: 비공개
@ -355,8 +455,11 @@ ko:
unlisted_long: 누구나 볼 수 있지만, 공개 타임라인에는 표시되지 않습니다 unlisted_long: 누구나 볼 수 있지만, 공개 타임라인에는 표시되지 않습니다
stream_entries: stream_entries:
click_to_show: 클릭해서 표시 click_to_show: 클릭해서 표시
pinned: 고정된 Toot
reblogged: 님이 부스트 했습니다 reblogged: 님이 부스트 했습니다
sensitive_content: 민감한 컨텐츠 sensitive_content: 민감한 컨텐츠
terms:
title: "%{instance} 이용약관과 개인정보 취급 방침"
time: time:
formats: formats:
default: "%Y년 %m월 %d일 %H:%M" default: "%Y년 %m월 %d일 %H:%M"
@ -379,3 +482,4 @@ ko:
users: users:
invalid_email: 메일 주소가 올바르지 않습니다 invalid_email: 메일 주소가 올바르지 않습니다
invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다 invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다
signed_in_as: '다음과 같이 로그인 중:'

View file

@ -337,6 +337,17 @@ nl:
reblog: reblog:
body: 'Jouw toot werd door %{name} geboost:' body: 'Jouw toot werd door %{name} geboost:'
subject: "%{name} boostte jouw toot" subject: "%{name} boostte jouw toot"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Volgende next: Volgende
prev: Vorige prev: Vorige

View file

@ -257,6 +257,17 @@
reblog: reblog:
body: 'Din status ble fremhevd av %{name}:' body: 'Din status ble fremhevd av %{name}:'
subject: "%{name} fremhevde din status" subject: "%{name} fremhevde din status"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Neste next: Neste
prev: Forrige prev: Forrige

View file

@ -103,6 +103,7 @@ oc:
title: Comptes title: Comptes
undo_silenced: Levar lo silenci undo_silenced: Levar lo silenci
undo_suspension: Levar la suspension undo_suspension: Levar la suspension
unsubscribe: Se desabonar
username: Nom dutilizaire username: Nom dutilizaire
web: Web web: Web
domain_blocks: domain_blocks:
@ -430,6 +431,17 @@ oc:
reblog: reblog:
body: "%{name} a tornat partejar vòstre estatut:" body: "%{name} a tornat partejar vòstre estatut:"
subject: "%{name} a tornat partejar vòstre estatut" subject: "%{name} a tornat partejar vòstre estatut"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Seguent next: Seguent
prev: Precedent prev: Precedent

View file

@ -355,6 +355,17 @@ pl:
reblog: reblog:
body: 'Twój wpis został podbity przez %{name}:' body: 'Twój wpis został podbity przez %{name}:'
subject: Twój wpis został podbity przez %{name} subject: Twój wpis został podbity przez %{name}
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Następna next: Następna
prev: Poprzednia prev: Poprzednia

View file

@ -255,6 +255,17 @@ pt-BR:
reblog: reblog:
body: 'O seu post foi reblogado por %{name}:' body: 'O seu post foi reblogado por %{name}:'
subject: "%{name} reblogou o seu post" subject: "%{name} reblogou o seu post"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Next next: Next
prev: Prev prev: Prev

View file

@ -182,6 +182,17 @@ pt:
reblog: reblog:
body: 'O teu post foi partilhado por %{name}:' body: 'O teu post foi partilhado por %{name}:'
subject: "%{name} partilhou o teu post" subject: "%{name} partilhou o teu post"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Seguinte next: Seguinte
prev: Anterior prev: Anterior

View file

@ -262,6 +262,17 @@ ru:
reblog: reblog:
body: 'Ваш статус был продвинут %{name}:' body: 'Ваш статус был продвинут %{name}:'
subject: "%{name} продвинул(а) Ваш статус" subject: "%{name} продвинул(а) Ваш статус"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: След next: След
prev: Пред prev: Пред

View file

@ -6,7 +6,7 @@ de:
avatar: PNG, GIF oder JPG. Maximal 2MB. Wird auf 120x120px herunterskaliert avatar: PNG, GIF oder JPG. Maximal 2MB. Wird auf 120x120px herunterskaliert
display_name: '<span class="name-counter">%{count}</span> Zeichen verbleiben' display_name: '<span class="name-counter">%{count}</span> Zeichen verbleiben'
header: PNG, GIF oder JPG. Maximal 2MB. Wird auf 700x335px herunterskaliert header: PNG, GIF oder JPG. Maximal 2MB. Wird auf 700x335px herunterskaliert
locked: Erlaubt dir, Nutzer zu überprüfen, bevor sie dir folgen können locked: Erlaubt dir, Profile zu überprüfen, bevor sie dir folgen können
note: '<span class="note-counter">%{count}</span> Zeichen verbleiben' note: '<span class="note-counter">%{count}</span> Zeichen verbleiben'
imports: imports:
data: CSV-Datei, die von einer anderen Mastodon-Instanz exportiert wurde data: CSV-Datei, die von einer anderen Mastodon-Instanz exportiert wurde
@ -33,10 +33,10 @@ de:
setting_default_privacy: Beitragsprivatspäre setting_default_privacy: Beitragsprivatspäre
severity: Gewichtung severity: Gewichtung
type: Importtyp type: Importtyp
username: Nutzername username: Profilname
interactions: interactions:
must_be_follower: Benachrichtigungen von Nicht-Folgern blockieren must_be_follower: Benachrichtigungen von Nicht-Folgern blockieren
must_be_following: Benachrichtigungen von Nutzern blockieren, denen ich nicht folge must_be_following: Benachrichtigungen von Profilen blockieren, denen ich nicht folge
notification_emails: notification_emails:
digest: Schicke Übersichts-E-Mails digest: Schicke Übersichts-E-Mails
favourite: E-Mail senden, wenn jemand meinen Beitrag favorisiert favourite: E-Mail senden, wenn jemand meinen Beitrag favorisiert

View file

@ -257,6 +257,17 @@ th:
reblog: reblog:
body: 'Your status was boosted by %{name}:' body: 'Your status was boosted by %{name}:'
subject: "%{name} boosted your status" subject: "%{name} boosted your status"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: ต่อไป next: ต่อไป
prev: ย้อนกลับ prev: ย้อนกลับ

View file

@ -255,6 +255,17 @@ tr:
reblog: reblog:
body: "%{name} durumunuzu boost etti:" body: "%{name} durumunuzu boost etti:"
subject: "%{name} durumunuzu boost etti" subject: "%{name} durumunuzu boost etti"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Sonraki next: Sonraki
prev: Önceki prev: Önceki

View file

@ -250,6 +250,17 @@ uk:
reblog: reblog:
body: 'Ваш статус було передмухнуто %{name}:' body: 'Ваш статус було передмухнуто %{name}:'
subject: "%{name} передмухнув ваш статус" subject: "%{name} передмухнув ваш статус"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Далі next: Далі
prev: Назад prev: Назад

View file

@ -261,6 +261,17 @@ zh-CN:
reblog: reblog:
body: 你的嘟文得到 %{name} 的转嘟 body: 你的嘟文得到 %{name} 的转嘟
subject: "%{name} 转嘟(嘟嘟滴)了你的嘟文" subject: "%{name} 转嘟(嘟嘟滴)了你的嘟文"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: 下一页 next: 下一页
prev: 上一页 prev: 上一页

View file

@ -256,6 +256,17 @@ zh-HK:
reblog: reblog:
body: 你的文章得到 %{name} 的轉推 body: 你的文章得到 %{name} 的轉推
subject: "%{name} 轉推了你的文章" subject: "%{name} 轉推了你的文章"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: 下一頁 next: 下一頁
prev: 上一頁 prev: 上一頁

View file

@ -211,6 +211,17 @@ zh-TW:
reblog: reblog:
body: 您的文章被 %{name} 轉推 body: 您的文章被 %{name} 轉推
subject: "%{name} 轉推了您的文章" subject: "%{name} 轉推了您的文章"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: 下一頁 next: 下一頁
prev: 上一頁 prev: 上一頁

View file

@ -0,0 +1,5 @@
class AddIndexIdAccountIdActivityTypeOnNotifications < ActiveRecord::Migration[5.1]
def change
add_index :notifications, [:id, :account_id, :activity_type], order: { id: :desc }
end
end

View file

@ -0,0 +1,5 @@
class AddLocalToStatuses < ActiveRecord::Migration[5.1]
def change
add_column :statuses, :local, :boolean, null: true, default: nil
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170901142658) do ActiveRecord::Schema.define(version: 20170905165803) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -180,6 +180,7 @@ ActiveRecord::Schema.define(version: 20170901142658) do
t.integer "from_account_id" t.integer "from_account_id"
t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true
t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type" t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type"
t.index ["id", "account_id", "activity_type"], name: "index_notifications_on_id_and_account_id_and_activity_type", order: { id: :desc }
end end
create_table "oauth_access_grants", id: :serial, force: :cascade do |t| create_table "oauth_access_grants", id: :serial, force: :cascade do |t|
@ -314,6 +315,7 @@ ActiveRecord::Schema.define(version: 20170901142658) do
t.integer "reblogs_count", default: 0, null: false t.integer "reblogs_count", default: 0, null: false
t.string "language" t.string "language"
t.bigint "conversation_id" t.bigint "conversation_id"
t.boolean "local"
t.index ["account_id", "id"], name: "index_statuses_on_account_id_id" t.index ["account_id", "id"], name: "index_statuses_on_account_id_id"
t.index ["conversation_id"], name: "index_statuses_on_conversation_id" t.index ["conversation_id"], name: "index_statuses_on_conversation_id"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id" t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
class Mastodon::UniqueRetryJobMiddleware
def call(_worker_class, item, _queue, _redis_pool)
return if item['unique_retry'] && retried?(item)
yield
end
private
def retried?(item)
# Use unique digest key of SidekiqUniqueJobs
unique_key = SidekiqUniqueJobs::UNIQUE_DIGEST_KEY
unique_digest = item[unique_key]
class_name = item['class']
retries = Sidekiq::RetrySet.new
retries.any? { |job| job.item['class'] == class_name && job.item[unique_key] == unique_digest }
end
end

View file

@ -9,11 +9,11 @@ module Mastodon
end end
def minor def minor
5 6
end end
def patch def patch
1 0
end end
def pre def pre
@ -21,7 +21,7 @@ module Mastodon
end end
def flags def flags
'' 'rc2'
end end
def to_a def to_a

View file

@ -273,10 +273,17 @@ namespace :mastodon do
desc 'Remove deprecated preview cards' desc 'Remove deprecated preview cards'
task remove_deprecated_preview_cards: :environment do task remove_deprecated_preview_cards: :environment do
return unless ActiveRecord::Base.connection.table_exists? 'deprecated_preview_cards' next unless ActiveRecord::Base.connection.table_exists? 'deprecated_preview_cards'
class DeprecatedPreviewCard < PreviewCard class DeprecatedPreviewCard < ActiveRecord::Base
self.table_name = 'deprecated_preview_cards' self.inheritance_column = false
path = '/preview_cards/:attachment/:id_partition/:style/:filename'
if ENV['S3_ENABLED'] != 'true'
path = (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + path
end
has_attached_file :image, styles: { original: '280x120>' }, convert_options: { all: '-quality 80 -strip' }, path: path
end end
puts 'Delete records and associated files from deprecated preview cards? [y/N]: ' puts 'Delete records and associated files from deprecated preview cards? [y/N]: '

View file

@ -85,7 +85,7 @@
"react-dom": "^15.6.1", "react-dom": "^15.6.1",
"react-immutable-proptypes": "^2.1.0", "react-immutable-proptypes": "^2.1.0",
"react-immutable-pure-component": "^1.0.0", "react-immutable-pure-component": "^1.0.0",
"react-intl": "^2.3.0", "react-intl": "^2.4.0",
"react-motion": "^0.5.0", "react-motion": "^0.5.0",
"react-notification": "^6.7.1", "react-notification": "^6.7.1",
"react-redux": "^5.0.4", "react-redux": "^5.0.4",

View file

@ -61,7 +61,29 @@ RSpec.describe AccountsController, type: :controller do
end end
end end
context 'html' do context 'html without since_id nor max_id' do
before do
get :show, params: { username: alice.username }
end
it 'assigns @account' do
expect(assigns(:account)).to eq alice
end
it 'assigns @pinned_statuses' do
pinned_statuses = assigns(:pinned_statuses).to_a
expect(pinned_statuses.size).to eq 3
expect(pinned_statuses[0]).to eq status7
expect(pinned_statuses[1]).to eq status5
expect(pinned_statuses[2]).to eq status6
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
end
context 'html with since_id and max_id' do
before do before do
get :show, params: { username: alice.username, max_id: status4.id, since_id: status1.id } get :show, params: { username: alice.username, max_id: status4.id, since_id: status1.id }
end end
@ -77,12 +99,9 @@ RSpec.describe AccountsController, type: :controller do
expect(statuses[1]).to eq status2 expect(statuses[1]).to eq status2
end end
it 'assigns @pinned_statuses' do it 'assigns an empty array to @pinned_statuses' do
pinned_statuses = assigns(:pinned_statuses).to_a pinned_statuses = assigns(:pinned_statuses).to_a
expect(pinned_statuses.size).to eq 3 expect(pinned_statuses.size).to eq 0
expect(pinned_statuses[0]).to eq status7
expect(pinned_statuses[1]).to eq status5
expect(pinned_statuses[2]).to eq status6
end end
it 'returns http success' do it 'returns http success' do

View file

@ -28,6 +28,13 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
end end
it 'returns JSON with following=true and requested=false' do
json = body_as_json
expect(json[:following]).to be true
expect(json[:requested]).to be false
end
it 'creates a following relation between user and target user' do it 'creates a following relation between user and target user' do
expect(user.account.following?(other_account)).to be true expect(user.account.following?(other_account)).to be true
end end

View file

@ -1,4 +1,8 @@
Fabricator(:status) do Fabricator(:status) do
account account
text "Lorem ipsum dolor sit amet" text "Lorem ipsum dolor sit amet"
after_build do |status|
status.uri = Faker::Internet.device_token if !status.account.local? && status.uri.nil?
end
end end

View file

@ -1,7 +1,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe ActivityPub::Activity::Delete do RSpec.describe ActivityPub::Activity::Delete do
let(:sender) { Fabricate(:account) } let(:sender) { Fabricate(:account, domain: 'example.com') }
let(:status) { Fabricate(:status, account: sender, uri: 'foobar') } let(:status) { Fabricate(:status, account: sender, uri: 'foobar') }
let(:json) do let(:json) do

View file

@ -1,7 +1,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe ActivityPub::Activity::Undo do RSpec.describe ActivityPub::Activity::Undo do
let(:sender) { Fabricate(:account) } let(:sender) { Fabricate(:account, domain: 'example.com') }
let(:json) do let(:json) do
{ {

View file

@ -178,7 +178,7 @@ RSpec.describe Formatter do
end end
context 'with remote status' do context 'with remote status' do
let(:status) { Fabricate(:status, text: 'Beep boop', uri: 'beepboop') } let(:status) { Fabricate(:status, account: remote_account, text: 'Beep boop') }
it 'reformats' do it 'reformats' do
is_expected.to eq 'Beep boop' is_expected.to eq 'Beep boop'
@ -226,7 +226,7 @@ RSpec.describe Formatter do
end end
context 'with remote status' do context 'with remote status' do
let(:status) { Fabricate(:status, text: '<script>alert("Hello")</script>', uri: 'beep boop') } let(:status) { Fabricate(:status, account: remote_account, text: '<script>alert("Hello")</script>') }
it 'returns tag-stripped text' do it 'returns tag-stripped text' do
is_expected.to eq '' is_expected.to eq ''

View file

@ -403,8 +403,7 @@ RSpec.describe OStatus::AtomSerializer do
it 'returns element whose rendered view triggers creation when processed' do it 'returns element whose rendered view triggers creation when processed' do
remote_account = Account.create!(username: 'username') remote_account = Account.create!(username: 'username')
remote_status = Fabricate(:status, account: remote_account) remote_status = Fabricate(:status, account: remote_account, created_at: '2000-01-01T00:00:00Z')
remote_status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true) entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true)
entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test
@ -421,7 +420,7 @@ RSpec.describe OStatus::AtomSerializer do
ProcessFeedService.new.call(xml, account) ProcessFeedService.new.call(xml, account)
expect(Status.find_by(uri: "tag:remote,2000-01-01:objectId=#{remote_status.id}:objectType=Status")).to be_instance_of Status expect(Status.find_by(uri: "https://remote/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status
end end
end end
@ -465,12 +464,11 @@ RSpec.describe OStatus::AtomSerializer do
end end
it 'appends id element with unique tag' do it 'appends id element with unique tag' do
status = Fabricate(:status, reblog_of_id: nil) status = Fabricate(:status, reblog_of_id: nil, created_at: '2000-01-01T00:00:00Z')
status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
entry = OStatus::AtomSerializer.new.entry(status.stream_entry) entry = OStatus::AtomSerializer.new.entry(status.stream_entry)
expect(entry.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status" expect(entry.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
end end
it 'appends published element with created date' do it 'appends published element with created date' do
@ -515,7 +513,7 @@ RSpec.describe OStatus::AtomSerializer do
entry = OStatus::AtomSerializer.new.entry(reblog.stream_entry) entry = OStatus::AtomSerializer.new.entry(reblog.stream_entry)
object = entry.nodes.find { |node| node.name == 'activity:object' } object = entry.nodes.find { |node| node.name == 'activity:object' }
expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{reblogged.id}:objectType=Status" expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{reblogged.account.to_param}/statuses/#{reblogged.id}"
end end
it 'does not append activity:object element if target is not present' do it 'does not append activity:object element if target is not present' do
@ -532,7 +530,7 @@ RSpec.describe OStatus::AtomSerializer do
link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
expect(link[:type]).to eq 'text/html' expect(link[:type]).to eq 'text/html'
expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username/updates/#{status.stream_entry.id}" expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
end end
it 'appends link element for itself' do it 'appends link element for itself' do
@ -553,7 +551,7 @@ RSpec.describe OStatus::AtomSerializer do
entry = OStatus::AtomSerializer.new.entry(reply_status.stream_entry) entry = OStatus::AtomSerializer.new.entry(reply_status.stream_entry)
in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' } in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
expect(in_reply_to[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{in_reply_to_status.id}:objectType=Status" expect(in_reply_to[:ref]).to eq "https://cb6e6126.ngrok.io/users/#{in_reply_to_status.account.to_param}/statuses/#{in_reply_to_status.id}"
end end
it 'does not append thr:in-reply-to element if not threaded' do it 'does not append thr:in-reply-to element if not threaded' do
@ -934,7 +932,7 @@ RSpec.describe OStatus::AtomSerializer do
favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
object = favourite_salmon.nodes.find { |node| node.name == 'activity:object' } object = favourite_salmon.nodes.find { |node| node.name == 'activity:object' }
expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status" expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
end end
it 'appends thr:in-reply-to element for status' do it 'appends thr:in-reply-to element for status' do
@ -945,7 +943,7 @@ RSpec.describe OStatus::AtomSerializer do
favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
in_reply_to = favourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' } in_reply_to = favourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status" expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
end end
@ -1034,7 +1032,7 @@ RSpec.describe OStatus::AtomSerializer do
unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
object = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object' } object = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object' }
expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status" expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
end end
it 'appends thr:in-reply-to element for status' do it 'appends thr:in-reply-to element for status' do
@ -1045,7 +1043,7 @@ RSpec.describe OStatus::AtomSerializer do
unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
in_reply_to = unfavourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' } in_reply_to = unfavourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status" expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
end end
@ -1453,7 +1451,7 @@ RSpec.describe OStatus::AtomSerializer do
it 'appends id element with URL for status' do it 'appends id element with URL for status' do
status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z')
object = OStatus::AtomSerializer.new.object(status) object = OStatus::AtomSerializer.new.object(status)
expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status" expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
end end
it 'appends published element with created date' do it 'appends published element with created date' do
@ -1463,7 +1461,8 @@ RSpec.describe OStatus::AtomSerializer do
end end
it 'appends updated element with updated date' do it 'appends updated element with updated date' do
status = Fabricate(:status, updated_at: '2000-01-01T00:00:00Z') status = Fabricate(:status)
status.updated_at = '2000-01-01T00:00:00Z'
object = OStatus::AtomSerializer.new.object(status) object = OStatus::AtomSerializer.new.object(status)
expect(object.updated.text).to eq '2000-01-01T00:00:00Z' expect(object.updated.text).to eq '2000-01-01T00:00:00Z'
end end
@ -1523,7 +1522,7 @@ RSpec.describe OStatus::AtomSerializer do
entry = OStatus::AtomSerializer.new.object(reply) entry = OStatus::AtomSerializer.new.object(reply)
in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' } in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{thread.id}:objectType=Status" expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{thread.account.to_param}/statuses/#{thread.id}"
expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{thread.id}" expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{thread.id}"
end end

View file

@ -157,23 +157,12 @@ RSpec.describe TagManager do
describe '#uri_for' do describe '#uri_for' do
subject { TagManager.instance.uri_for(target) } subject { TagManager.instance.uri_for(target) }
context 'activity object' do
let(:target) { Fabricate(:status, reblog: Fabricate(:status)).stream_entry }
before { target.update!(created_at: '2000-01-01T00:00:00Z') }
it 'returns the unique tag for status' do
expect(target.object_type).to eq :activity
is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
end
end
context 'comment object' do context 'comment object' do
let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) } let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) }
it 'returns the unique tag for status' do it 'returns the unique tag for status' do
expect(target.object_type).to eq :comment expect(target.object_type).to eq :comment
is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status" is_expected.to eq target.uri
end end
end end
@ -182,7 +171,7 @@ RSpec.describe TagManager do
it 'returns the unique tag for status' do it 'returns the unique tag for status' do
expect(target.object_type).to eq :note expect(target.object_type).to eq :note
is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status" is_expected.to eq target.uri
end end
end end

View file

@ -13,9 +13,15 @@ RSpec.describe Status, type: :model do
end end
it 'returns false if a remote URI is set' do it 'returns false if a remote URI is set' do
subject.uri = 'a' alice.update(domain: 'example.com')
subject.save
expect(subject.local?).to be false expect(subject.local?).to be false
end end
it 'returns true if a URI is set and `local` is true' do
subject.update(uri: 'example.com', local: true)
expect(subject.local?).to be true
end
end end
describe '#reblog?' do describe '#reblog?' do
@ -495,7 +501,7 @@ RSpec.describe Status, type: :model do
end end
end end
describe 'before_create' do describe 'before_validation' do
it 'sets account being replied to correctly over intermediary nodes' do it 'sets account being replied to correctly over intermediary nodes' do
first_status = Fabricate(:status, account: bob) first_status = Fabricate(:status, account: bob)
intermediary = Fabricate(:status, thread: first_status, account: alice) intermediary = Fabricate(:status, thread: first_status, account: alice)
@ -512,5 +518,22 @@ RSpec.describe Status, type: :model do
parent = Fabricate(:status, text: 'First') parent = Fabricate(:status, text: 'First')
expect(Status.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id expect(Status.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id
end end
it 'sets `local` to true for status by local account' do
expect(Status.create(account: alice, text: 'foo').local).to be true
end
it 'sets `local` to false for status by remote account' do
alice.update(domain: 'example.com')
expect(Status.create(account: alice, text: 'foo').local).to be false
end
end
describe 'after_create' do
it 'saves ActivityPub uri as uri for local status' do
status = Status.create(account: alice, text: 'foo')
status.reload
expect(status.uri).to start_with('https://')
end
end end
end end

View file

@ -55,7 +55,7 @@ RSpec.describe FetchLinkCardService do
end end
context 'in a remote status' do context 'in a remote status' do
let(:status) { Fabricate(:status, uri: 'abc', text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') } let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }
it 'parses out URLs' do it 'parses out URLs' do
expect(a_request(:head, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once expect(a_request(:head, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once

View file

@ -403,11 +403,11 @@ const startWorker = (workerId) => {
}); });
app.get('/api/v1/streaming/hashtag', (req, res) => { app.get('/api/v1/streaming/hashtag', (req, res) => {
streamFrom(`timeline:hashtag:${req.query.tag}`, req, streamToHttp(req, res), streamHttpEnd(req), true); streamFrom(`timeline:hashtag:${req.query.tag.toLowerCase()}`, req, streamToHttp(req, res), streamHttpEnd(req), true);
}); });
app.get('/api/v1/streaming/hashtag/local', (req, res) => { app.get('/api/v1/streaming/hashtag/local', (req, res) => {
streamFrom(`timeline:hashtag:${req.query.tag}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true); streamFrom(`timeline:hashtag:${req.query.tag.toLowerCase()}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true);
}); });
const wss = new WebSocket.Server({ server, verifyClient: wsVerifyClient }); const wss = new WebSocket.Server({ server, verifyClient: wsVerifyClient });
@ -438,10 +438,10 @@ const startWorker = (workerId) => {
streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break; break;
case 'hashtag': case 'hashtag':
streamFrom(`timeline:hashtag:${location.query.tag}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break; break;
case 'hashtag:local': case 'hashtag:local':
streamFrom(`timeline:hashtag:${location.query.tag}:local`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}:local`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break; break;
default: default:
ws.close(); ws.close();

View file

@ -3130,23 +3130,17 @@ intl-messageformat-parser@^1.2.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.3.0.tgz#c5d26ffb894c7d9c2b9fa444c67f417ab2594268" resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.3.0.tgz#c5d26ffb894c7d9c2b9fa444c67f417ab2594268"
intl-messageformat@1.3.0, intl-messageformat@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-1.3.0.tgz#f7d926aded7a3ab19b2dc601efd54e99a4bd4eae"
dependencies:
intl-messageformat-parser "1.2.0"
intl-messageformat@^2.0.0: intl-messageformat@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.0.0.tgz#3d56982583425aee23b76c8b985fb9b0aae5be3c" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.0.0.tgz#3d56982583425aee23b76c8b985fb9b0aae5be3c"
dependencies: dependencies:
intl-messageformat-parser "1.2.0" intl-messageformat-parser "1.2.0"
intl-relativeformat@^1.3.0: intl-messageformat@^2.1.0:
version "1.3.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-1.3.0.tgz#893dc7076fccd380cf091a2300c380fa57ace45b" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.1.0.tgz#1c51da76f02a3f7b360654cdc51bbc4d3fa6c72c"
dependencies: dependencies:
intl-messageformat "1.3.0" intl-messageformat-parser "1.2.0"
intl-relativeformat@^2.0.0: intl-relativeformat@^2.0.0:
version "2.0.0" version "2.0.0"
@ -5312,13 +5306,13 @@ react-intl-translations-manager@^5.0.0:
json-stable-stringify "^1.0.1" json-stable-stringify "^1.0.1"
mkdirp "^0.5.1" mkdirp "^0.5.1"
react-intl@^2.3.0: react-intl@^2.4.0:
version "2.3.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.3.0.tgz#e1df6af5667fdf01cbe4aab20e137251e2ae5142" resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.4.0.tgz#66c14dc9df9a73b2fbbfbd6021726e80a613eb15"
dependencies: dependencies:
intl-format-cache "^2.0.5" intl-format-cache "^2.0.5"
intl-messageformat "^1.3.0" intl-messageformat "^2.1.0"
intl-relativeformat "^1.3.0" intl-relativeformat "^2.0.0"
invariant "^2.1.1" invariant "^2.1.1"
react-motion@^0.5.0: react-motion@^0.5.0: