diff options
Diffstat (limited to 'app')
149 files changed, 2692 insertions, 851 deletions
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb new file mode 100644 index 000000000..572ad1ac2 --- /dev/null +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Admin + class CustomEmojisController < BaseController + def index + @custom_emojis = CustomEmoji.where(domain: nil) + end + + def new + @custom_emoji = CustomEmoji.new + end + + def create + @custom_emoji = CustomEmoji.new(resource_params) + + if @custom_emoji.save + redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg') + else + render :new + end + end + + def destroy + CustomEmoji.find(params[:id]).destroy + redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg') + end + + private + + def resource_params + params.require(:custom_emoji).permit(:shortcode, :image) + end + end +end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 3296e08db..22f02e5d0 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -14,8 +14,12 @@ module Admin private + def filtered_instances + InstanceFilter.new(filter_params).results + end + def paginated_instances - Account.remote.by_domain_accounts.page(params[:page]) + filtered_instances.page(params[:page]) end helper_method :paginated_instances @@ -27,5 +31,11 @@ module Admin def subscribeable_accounts Account.with_followers.remote.where(domain: params[:by_domain]) end + + def filter_params + params.permit( + :domain_name + ) + end end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index c5e6fe4e5..a2f86b8a9 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -14,6 +14,7 @@ module Admin open_deletion timeline_preview bootstrap_timeline_accounts + thumbnail ).freeze BOOLEAN_SETTINGS = %w( @@ -22,14 +23,23 @@ module Admin timeline_preview ).freeze + UPLOAD_SETTINGS = %w( + thumbnail + ).freeze + def edit @admin_settings = Form::AdminSettings.new end def update settings_params.each do |key, value| - setting = Setting.where(var: key).first_or_initialize(var: key) - setting.update(value: value_for_update(key, value)) + if UPLOAD_SETTINGS.include?(key) + upload = SiteUpload.where(var: key).first_or_initialize(var: key) + upload.update(file: value) + else + setting = Setting.where(var: key).first_or_initialize(var: key) + setting.update(value: value_for_update(key, value)) + end end flash[:notice] = I18n.t('generic.changes_saved_msg') diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index fbfb5473e..ad7f09f34 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -12,7 +12,30 @@ class HomeController < ApplicationController private def authenticate_user! - redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in? + return if user_signed_in? + + matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/) + + if matches + case matches[1] + when 'statuses' + status = Status.find_by(id: matches[2]) + + if status && (status.public_visibility? || status.unlisted_visibility?) + redirect_to(ActivityPub::TagManager.instance.url_for(status)) + return + end + when 'accounts' + account = Account.find_by(id: matches[2]) + + if account + redirect_to(ActivityPub::TagManager.instance.url_for(account)) + return + end + end + end + + redirect_to(default_redirect_path) end def set_initial_state_json @@ -29,4 +52,14 @@ class HomeController < ApplicationController admin: Account.find_local(Setting.site_contact_username), } end + + def default_redirect_path + if request.path.start_with?('/web') + new_user_session_path + elsif single_user_mode? + short_account_path(Account.first) + else + about_path + end + end end diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb new file mode 100644 index 000000000..155670837 --- /dev/null +++ b/app/controllers/media_proxy_controller.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class MediaProxyController < ApplicationController + include RoutingHelper + + def show + RedisLock.acquire(lock_options) do |lock| + if lock.acquired? + @media_attachment = MediaAttachment.remote.find(params[:id]) + redownload! if @media_attachment.needs_redownload? && !reject_media? + end + end + + redirect_to full_asset_url(@media_attachment.file.url(version)) + end + + private + + def redownload! + @media_attachment.file_remote_url = @media_attachment.remote_url + @media_attachment.created_at = Time.now.utc + @media_attachment.save! + end + + def version + if request.path.ends_with?('/small') + :small + else + :original + end + end + + def lock_options + { redis: Redis.current, key: "media_download:#{params[:id]}" } + end + + def reject_media? + DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media? + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 61d4442c1..6d625e7db 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -42,4 +42,8 @@ module ApplicationHelper content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end + + def opengraph(property, content) + tag(:meta, content: content, property: property) + end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 369a45680..14776b354 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -41,7 +41,7 @@ module SettingsHelper end def filterable_languages - I18n.available_locales.map { |locale| locale.to_s.split('-').first.to_sym }.uniq + LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?)) end def hash_to_object(hash) diff --git a/app/javascript/images/logo.svg b/app/javascript/images/logo.svg index 4b72b3ac8..034a9c221 100644 --- a/app/javascript/images/logo.svg +++ b/app/javascript/images/logo.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="61.076954mm" height="65.47831mm" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg> diff --git a/app/javascript/images/logo_alt.svg b/app/javascript/images/logo_alt.svg index e88ca7418..102d4c787 100644 --- a/app/javascript/images/logo_alt.svg +++ b/app/javascript/images/logo_alt.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="61.077141mm" height="65.47831mm" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg> diff --git a/app/javascript/images/logo_full.svg b/app/javascript/images/logo_full.svg index 8b1328e8c..c33883342 100644 --- a/app/javascript/images/logo_full.svg +++ b/app/javascript/images/logo_full.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 713.35878 175.8678" height="49.633801mm" width="201.3257mm"><path d="M160.55476 105.43125c-2.4125 12.40625-21.5975 25.9825-43.63375 28.61375-11.49125 1.3725-22.80375 2.63125-34.8675 2.07875-19.73-.90375-35.2975-4.71-35.2975-4.71 0 1.92125.11875 3.75.355 5.46 2.565 19.47 19.3075 20.6375 35.16625 21.18125 16.00625.5475 30.2575-3.9475 30.2575-3.9475l.65875 14.4725s-11.19625 6.01125-31.14 7.11625c-10.99875.605-24.65375-.27625-40.56-4.485C6.99851 162.08 1.06601 125.31.15851 88-.11899 76.9225.05226 66.47625.05226 57.74125c0-38.1525 24.99625-49.335 24.99625-49.335C37.65226 2.6175 59.27976.18375 81.76351 0h.5525c22.48375.18375 44.125 2.6175 56.72875 8.40625 0 0 24.99625 11.1825 24.99625 49.335 0 0 .3125 28.1475-3.48625 47.69" fill="#3088d4"/><path d="M34.65751 48.494c0-5.55375 4.5025-10.055 10.055-10.055 5.55375 0 10.055 4.50125 10.055 10.055 0 5.5525-4.50125 10.055-10.055 10.055-5.5525 0-10.055-4.5025-10.055-10.055M178.86476 60.69975v46.195h-18.30125v-44.8375c0-9.4525-3.9775-14.24875-11.9325-14.24875-8.79375 0-13.2025 5.69125-13.2025 16.94375V89.2935h-18.19375V64.75225c0-11.2525-4.40875-16.94375-13.2025-16.94375-7.955 0-11.9325 4.79625-11.9325 14.24875v44.8375H73.79851v-46.195c0-9.44125 2.40375-16.94375 7.2325-22.495 4.98-5.55 11.50125-8.395 19.595-8.395 9.36625 0 16.45875 3.59875 21.14625 10.79875l4.56 7.6425 4.55875-7.6425c4.68875-7.2 11.78-10.79875 21.1475-10.79875 8.09375 0 14.61375 2.845 19.59375 8.395 4.82875 5.55125 7.2325 13.05375 7.2325 22.495M241.91276 83.663625c3.77625-3.99 5.595-9.015 5.595-15.075 0-6.06-1.81875-11.085-5.595-14.9275-3.63625-3.99125-8.25375-5.91125-13.84875-5.91125-5.59625 0-10.2125 1.92-13.84875 5.91125-3.6375 3.8425-5.45625 8.8675-5.45625 14.9275 0 6.06 1.81875 11.085 5.45625 15.075 3.63625 3.8425 8.2525 5.76375 13.84875 5.76375 5.595 0 10.2125-1.92125 13.84875-5.76375m5.595-52.025h18.04625v73.9h-18.04625v-8.72125c-5.455 7.2425-13.01 10.79-22.80125 10.79-9.3725 0-17.34625-3.695-24.06125-11.23375-6.57375-7.5375-9.93125-16.84875-9.93125-27.785 0-10.78875 3.3575-20.10125 9.93125-27.63875 6.715-7.5375 14.68875-11.38 24.06125-11.38 9.79125 0 17.34625 3.5475 22.80125 10.78875v-8.72zM326.26951 67.258625c5.315 3.99 7.97375 9.60625 7.83375 16.7 0 7.53875-2.65875 13.45-8.11375 17.58875-5.45625 3.99125-12.03 6.06-20.00375 6.06-14.40875 0-24.20125-5.9125-29.3775-17.58875l15.66875-9.31c2.0975 6.35375 6.71375 9.60625 13.70875 9.60625 6.43375 0 9.6525-2.07 9.6525-6.35625 0-3.10375-4.1975-5.91125-12.73-8.1275-3.21875-.8875-5.87625-1.77375-7.97375-2.51375-2.9375-1.18125-5.455-2.5125-7.55375-4.1375-5.17625-3.99-7.83375-9.3125-7.83375-16.11 0-7.2425 2.5175-13.00625 7.55375-17.145 5.17625-4.28625 11.47-6.355 19.025-6.355 12.03 0 20.84375 5.1725 26.5775 15.66625l-15.38625 8.8675c-2.23875-5.02375-6.015-7.53625-11.19125-7.53625-5.45625 0-8.11375 2.06875-8.11375 6.05875 0 3.10375 4.19625 5.91125 12.73 8.12875 6.575 1.4775 11.75 3.695 15.5275 6.50375M383.626635 49.966125h-15.8075v30.7425c0 3.695 1.4 5.91125 4.0575 6.945 1.95875.74 5.875.8875 11.75.59125v17.29375c-12.16875 1.4775-20.9825.295-26.15875-3.69625-5.175-3.8425-7.69375-10.93625-7.69375-21.13375v-30.7425h-12.17v-18.3275h12.17v-14.9275l18.045-5.76375v20.69125h15.8075v18.3275zM441.124885 83.2205c3.6375-3.84375 5.455-8.72125 5.455-14.6325 0-5.91125-1.8175-10.78875-5.455-14.63125-3.6375-3.84375-8.11375-5.76375-13.57-5.76375-5.455 0-9.93125 1.92-13.56875 5.76375-3.4975 3.99-5.31625 8.8675-5.31625 14.63125 0 5.765 1.81875 10.6425 5.31625 14.6325 3.6375 3.8425 8.11375 5.76375 13.56875 5.76375 5.45625 0 9.9325-1.92125 13.57-5.76375m-39.86875 13.15375c-7.13375-7.5375-10.63125-16.70125-10.63125-27.78625 0-10.9375 3.4975-20.1 10.63125-27.6375 7.13375-7.5375 15.9475-11.38 26.29875-11.38 10.3525 0 19.165 3.8425 26.3 11.38 7.135 7.5375 10.77125 16.84875 10.77125 27.6375 0 10.9375-3.63625 20.24875-10.77125 27.78625-7.135 7.53875-15.8075 11.2325-26.3 11.2325-10.49125 0-19.165-3.69375-26.29875-11.2325M524.92126 83.663625c3.6375-3.99 5.455-9.015 5.455-15.075 0-6.06-1.8175-11.085-5.455-14.9275-3.63625-3.99125-8.25375-5.91125-13.84875-5.91125-5.59625 0-10.2125 1.92-13.98875 5.91125-3.63625 3.8425-5.45625 8.8675-5.45625 14.9275 0 6.06 1.82 11.085 5.45625 15.075 3.77625 3.8425 8.5325 5.76375 13.98875 5.76375 5.595 0 10.2125-1.92125 13.84875-5.76375m5.455-81.585h18.04625v103.46h-18.04625v-8.72125c-5.315 7.2425-12.87 10.79-22.66125 10.79-9.3725 0-17.485-3.695-24.2-11.23375-6.575-7.5375-9.9325-16.84875-9.9325-27.785 0-10.78875 3.3575-20.10125 9.9325-27.63875 6.715-7.5375 14.8275-11.38 24.2-11.38 9.79125 0 17.34625 3.5475 22.66125 10.78875v-38.28zM611.79626 83.2205c3.63625-3.84375 5.455-8.72125 5.455-14.6325 0-5.91125-1.81875-10.78875-5.455-14.63125-3.6375-3.84375-8.11375-5.76375-13.57-5.76375-5.455 0-9.9325 1.92-13.56875 5.76375-3.49875 3.99-5.31625 8.8675-5.31625 14.63125 0 5.765 1.8175 10.6425 5.31625 14.6325 3.63625 3.8425 8.11375 5.76375 13.56875 5.76375 5.45625 0 9.9325-1.92125 13.57-5.76375m-39.86875 13.15375c-7.135-7.5375-10.63125-16.70125-10.63125-27.78625 0-10.9375 3.49625-20.1 10.63125-27.6375 7.135-7.5375 15.9475-11.38 26.29875-11.38 10.3525 0 19.165 3.8425 26.3 11.38 7.135 7.5375 10.77125 16.84875 10.77125 27.6375 0 10.9375-3.63625 20.24875-10.77125 27.78625-7.135 7.53875-15.8075 11.2325-26.3 11.2325-10.49125 0-19.16375-3.69375-26.29875-11.2325M713.35876 60.163875v45.37375h-18.04625v-43.00875c0-4.8775-1.25875-8.5725-3.77625-11.38-2.37875-2.5125-5.73625-3.84375-10.0725-3.84375-10.2125 0-15.3875 6.06-15.3875 18.3275v39.905h-18.04625v-73.89875h18.04625v8.27625c4.33625-6.94625 11.19-10.345 20.84375-10.345 7.69375 0 13.98875 2.66 18.885 8.12875 5.035 5.46875 7.55375 12.85875 7.55375 22.465" fill="#fff"/></svg> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 713.35878 175.8678"><path d="M160.55476 105.43125c-2.4125 12.40625-21.5975 25.9825-43.63375 28.61375-11.49125 1.3725-22.80375 2.63125-34.8675 2.07875-19.73-.90375-35.2975-4.71-35.2975-4.71 0 1.92125.11875 3.75.355 5.46 2.565 19.47 19.3075 20.6375 35.16625 21.18125 16.00625.5475 30.2575-3.9475 30.2575-3.9475l.65875 14.4725s-11.19625 6.01125-31.14 7.11625c-10.99875.605-24.65375-.27625-40.56-4.485C6.99851 162.08 1.06601 125.31.15851 88-.11899 76.9225.05226 66.47625.05226 57.74125c0-38.1525 24.99625-49.335 24.99625-49.335C37.65226 2.6175 59.27976.18375 81.76351 0h.5525c22.48375.18375 44.125 2.6175 56.72875 8.40625 0 0 24.99625 11.1825 24.99625 49.335 0 0 .3125 28.1475-3.48625 47.69" fill="#3088d4"/><path d="M34.65751 48.494c0-5.55375 4.5025-10.055 10.055-10.055 5.55375 0 10.055 4.50125 10.055 10.055 0 5.5525-4.50125 10.055-10.055 10.055-5.5525 0-10.055-4.5025-10.055-10.055M178.86476 60.69975v46.195h-18.30125v-44.8375c0-9.4525-3.9775-14.24875-11.9325-14.24875-8.79375 0-13.2025 5.69125-13.2025 16.94375V89.2935h-18.19375V64.75225c0-11.2525-4.40875-16.94375-13.2025-16.94375-7.955 0-11.9325 4.79625-11.9325 14.24875v44.8375H73.79851v-46.195c0-9.44125 2.40375-16.94375 7.2325-22.495 4.98-5.55 11.50125-8.395 19.595-8.395 9.36625 0 16.45875 3.59875 21.14625 10.79875l4.56 7.6425 4.55875-7.6425c4.68875-7.2 11.78-10.79875 21.1475-10.79875 8.09375 0 14.61375 2.845 19.59375 8.395 4.82875 5.55125 7.2325 13.05375 7.2325 22.495M241.91276 83.663625c3.77625-3.99 5.595-9.015 5.595-15.075 0-6.06-1.81875-11.085-5.595-14.9275-3.63625-3.99125-8.25375-5.91125-13.84875-5.91125-5.59625 0-10.2125 1.92-13.84875 5.91125-3.6375 3.8425-5.45625 8.8675-5.45625 14.9275 0 6.06 1.81875 11.085 5.45625 15.075 3.63625 3.8425 8.2525 5.76375 13.84875 5.76375 5.595 0 10.2125-1.92125 13.84875-5.76375m5.595-52.025h18.04625v73.9h-18.04625v-8.72125c-5.455 7.2425-13.01 10.79-22.80125 10.79-9.3725 0-17.34625-3.695-24.06125-11.23375-6.57375-7.5375-9.93125-16.84875-9.93125-27.785 0-10.78875 3.3575-20.10125 9.93125-27.63875 6.715-7.5375 14.68875-11.38 24.06125-11.38 9.79125 0 17.34625 3.5475 22.80125 10.78875v-8.72zM326.26951 67.258625c5.315 3.99 7.97375 9.60625 7.83375 16.7 0 7.53875-2.65875 13.45-8.11375 17.58875-5.45625 3.99125-12.03 6.06-20.00375 6.06-14.40875 0-24.20125-5.9125-29.3775-17.58875l15.66875-9.31c2.0975 6.35375 6.71375 9.60625 13.70875 9.60625 6.43375 0 9.6525-2.07 9.6525-6.35625 0-3.10375-4.1975-5.91125-12.73-8.1275-3.21875-.8875-5.87625-1.77375-7.97375-2.51375-2.9375-1.18125-5.455-2.5125-7.55375-4.1375-5.17625-3.99-7.83375-9.3125-7.83375-16.11 0-7.2425 2.5175-13.00625 7.55375-17.145 5.17625-4.28625 11.47-6.355 19.025-6.355 12.03 0 20.84375 5.1725 26.5775 15.66625l-15.38625 8.8675c-2.23875-5.02375-6.015-7.53625-11.19125-7.53625-5.45625 0-8.11375 2.06875-8.11375 6.05875 0 3.10375 4.19625 5.91125 12.73 8.12875 6.575 1.4775 11.75 3.695 15.5275 6.50375M383.626635 49.966125h-15.8075v30.7425c0 3.695 1.4 5.91125 4.0575 6.945 1.95875.74 5.875.8875 11.75.59125v17.29375c-12.16875 1.4775-20.9825.295-26.15875-3.69625-5.175-3.8425-7.69375-10.93625-7.69375-21.13375v-30.7425h-12.17v-18.3275h12.17v-14.9275l18.045-5.76375v20.69125h15.8075v18.3275zM441.124885 83.2205c3.6375-3.84375 5.455-8.72125 5.455-14.6325 0-5.91125-1.8175-10.78875-5.455-14.63125-3.6375-3.84375-8.11375-5.76375-13.57-5.76375-5.455 0-9.93125 1.92-13.56875 5.76375-3.4975 3.99-5.31625 8.8675-5.31625 14.63125 0 5.765 1.81875 10.6425 5.31625 14.6325 3.6375 3.8425 8.11375 5.76375 13.56875 5.76375 5.45625 0 9.9325-1.92125 13.57-5.76375m-39.86875 13.15375c-7.13375-7.5375-10.63125-16.70125-10.63125-27.78625 0-10.9375 3.4975-20.1 10.63125-27.6375 7.13375-7.5375 15.9475-11.38 26.29875-11.38 10.3525 0 19.165 3.8425 26.3 11.38 7.135 7.5375 10.77125 16.84875 10.77125 27.6375 0 10.9375-3.63625 20.24875-10.77125 27.78625-7.135 7.53875-15.8075 11.2325-26.3 11.2325-10.49125 0-19.165-3.69375-26.29875-11.2325M524.92126 83.663625c3.6375-3.99 5.455-9.015 5.455-15.075 0-6.06-1.8175-11.085-5.455-14.9275-3.63625-3.99125-8.25375-5.91125-13.84875-5.91125-5.59625 0-10.2125 1.92-13.98875 5.91125-3.63625 3.8425-5.45625 8.8675-5.45625 14.9275 0 6.06 1.82 11.085 5.45625 15.075 3.77625 3.8425 8.5325 5.76375 13.98875 5.76375 5.595 0 10.2125-1.92125 13.84875-5.76375m5.455-81.585h18.04625v103.46h-18.04625v-8.72125c-5.315 7.2425-12.87 10.79-22.66125 10.79-9.3725 0-17.485-3.695-24.2-11.23375-6.575-7.5375-9.9325-16.84875-9.9325-27.785 0-10.78875 3.3575-20.10125 9.9325-27.63875 6.715-7.5375 14.8275-11.38 24.2-11.38 9.79125 0 17.34625 3.5475 22.66125 10.78875v-38.28zM611.79626 83.2205c3.63625-3.84375 5.455-8.72125 5.455-14.6325 0-5.91125-1.81875-10.78875-5.455-14.63125-3.6375-3.84375-8.11375-5.76375-13.57-5.76375-5.455 0-9.9325 1.92-13.56875 5.76375-3.49875 3.99-5.31625 8.8675-5.31625 14.63125 0 5.765 1.8175 10.6425 5.31625 14.6325 3.63625 3.8425 8.11375 5.76375 13.56875 5.76375 5.45625 0 9.9325-1.92125 13.57-5.76375m-39.86875 13.15375c-7.135-7.5375-10.63125-16.70125-10.63125-27.78625 0-10.9375 3.49625-20.1 10.63125-27.6375 7.135-7.5375 15.9475-11.38 26.29875-11.38 10.3525 0 19.165 3.8425 26.3 11.38 7.135 7.5375 10.77125 16.84875 10.77125 27.6375 0 10.9375-3.63625 20.24875-10.77125 27.78625-7.135 7.53875-15.8075 11.2325-26.3 11.2325-10.49125 0-19.16375-3.69375-26.29875-11.2325M713.35876 60.163875v45.37375h-18.04625v-43.00875c0-4.8775-1.25875-8.5725-3.77625-11.38-2.37875-2.5125-5.73625-3.84375-10.0725-3.84375-10.2125 0-15.3875 6.06-15.3875 18.3275v39.905h-18.04625v-73.89875h18.04625v8.27625c4.33625-6.94625 11.19-10.345 20.84375-10.345 7.69375 0 13.98875 2.66 18.885 8.12875 5.035 5.46875 7.55375 12.85875 7.55375 22.465" fill="#fff"/></svg> diff --git a/app/javascript/images/mastodon_small.jpg b/app/javascript/images/mastodon_small.jpg deleted file mode 100644 index 9c88ce3f7..000000000 --- a/app/javascript/images/mastodon_small.jpg +++ /dev/null Binary files differdiff --git a/app/javascript/images/preview.jpg b/app/javascript/images/preview.jpg new file mode 100644 index 000000000..ec2856748 --- /dev/null +++ b/app/javascript/images/preview.jpg Binary files differdiff --git a/app/javascript/mastodon/actions/height_cache.js b/app/javascript/mastodon/actions/height_cache.js new file mode 100644 index 000000000..4c752993f --- /dev/null +++ b/app/javascript/mastodon/actions/height_cache.js @@ -0,0 +1,17 @@ +export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET'; +export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR'; + +export function setHeight (key, id, height) { + return { + type: HEIGHT_CACHE_SET, + key, + id, + height, + }; +}; + +export function clearHeight () { + return { + type: HEIGHT_CACHE_CLEAR, + }; +}; diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 0b5e72c17..2204e0b14 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -23,9 +23,6 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; -export const STATUS_SET_HEIGHT = 'STATUS_SET_HEIGHT'; -export const STATUSES_CLEAR_HEIGHT = 'STATUSES_CLEAR_HEIGHT'; - export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -218,17 +215,3 @@ export function unmuteStatusFail(id, error) { error, }; }; - -export function setStatusHeight (id, height) { - return { - type: STATUS_SET_HEIGHT, - id, - height, - }; -}; - -export function clearStatusesHeight () { - return { - type: STATUSES_CLEAR_HEIGHT, - }; -}; diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js index 347767818..bb83a4da0 100644 --- a/app/javascript/mastodon/components/intersection_observer_article.js +++ b/app/javascript/mastodon/components/intersection_observer_article.js @@ -7,10 +7,13 @@ import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; export default class IntersectionObserverArticle extends ImmutablePureComponent { static propTypes = { - intersectionObserverWrapper: PropTypes.object, + intersectionObserverWrapper: PropTypes.object.isRequired, id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + saveHeightKey: PropTypes.string, + cachedHeight: PropTypes.number, + onHeightChange: PropTypes.func, children: PropTypes.node, }; @@ -34,13 +37,10 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent } componentDidMount () { - if (!this.props.intersectionObserverWrapper) { - // TODO: enable IntersectionObserver optimization for notification statuses. - // These are managed in notifications/index.js rather than status_list.js - return; - } - this.props.intersectionObserverWrapper.observe( - this.props.id, + const { intersectionObserverWrapper, id } = this.props; + + intersectionObserverWrapper.observe( + id, this.node, this.handleIntersection ); @@ -49,20 +49,21 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent } componentWillUnmount () { - if (this.props.intersectionObserverWrapper) { - this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node); - } + const { intersectionObserverWrapper, id } = this.props; + intersectionObserverWrapper.unobserve(id, this.node); this.componentMounted = false; } handleIntersection = (entry) => { + const { onHeightChange, saveHeightKey, id } = this.props; + if (this.node && this.node.children.length !== 0) { // save the height of the fully-rendered element this.height = getRectFromEntry(entry).height; - if (this.props.onHeightChange) { - this.props.onHeightChange(this.props.status, this.height); + if (onHeightChange && saveHeightKey) { + onHeightChange(saveHeightKey, id, this.height); } } @@ -94,16 +95,16 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent } render () { - const { children, id, index, listLength } = this.props; + const { children, id, index, listLength, cachedHeight } = this.props; const { isIntersecting, isHidden } = this.state; - if (!isIntersecting && isHidden) { + if (!isIntersecting && (isHidden || cachedHeight)) { return ( <article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} - style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }} + style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }} data-id={id} tabIndex='0' > diff --git a/app/javascript/mastodon/components/load_more.js b/app/javascript/mastodon/components/load_more.js index e2fe1fed7..c4c8c94a2 100644 --- a/app/javascript/mastodon/components/load_more.js +++ b/app/javascript/mastodon/components/load_more.js @@ -17,7 +17,7 @@ export default class LoadMore extends React.PureComponent { const { visible } = this.props; return ( - <button className='load-more' disabled={!visible} style={{ opacity: visible ? 1 : 0 }} onClick={this.props.onClick}> + <button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}> <FormattedMessage id='status.load_more' defaultMessage='Load more' /> </button> ); diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index fa6ea72d5..a03b682b2 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -122,8 +122,8 @@ class Item extends React.PureComponent { const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; - const srcSet = hasSize && `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`; - const sizes = hasSize && `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`; + const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null; + const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null; thumbnail = ( <a diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 723dd322b..ff0540e5d 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { ScrollContainer } from 'react-router-scroll'; import PropTypes from 'prop-types'; -import IntersectionObserverArticle from './intersection_observer_article'; +import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container'; import LoadMore from './load_more'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; @@ -9,6 +9,10 @@ import { List as ImmutableList } from 'immutable'; export default class ScrollableList extends PureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { scrollKey: PropTypes.string.isRequired, onScrollToBottom: PropTypes.func, @@ -163,7 +167,7 @@ export default class ScrollableList extends PureComponent { const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; const childrenCount = React.Children.count(children); - const loadMore = <LoadMore visible={!isLoading && childrenCount > 0 && hasMore} onClick={this.handleLoadMore} />; + const loadMore = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null; let scrollableArea = null; if (isLoading || childrenCount > 0 || !emptyMessage) { @@ -173,9 +177,16 @@ export default class ScrollableList extends PureComponent { {prepend} {React.Children.map(this.props.children, (child, index) => ( - <IntersectionObserverArticle key={child.key} id={child.key} index={index} listLength={childrenCount} intersectionObserverWrapper={this.intersectionObserverWrapper}> + <IntersectionObserverArticleContainer + key={child.key} + id={child.key} + index={index} + listLength={childrenCount} + intersectionObserverWrapper={this.intersectionObserverWrapper} + saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null} + > {child} - </IntersectionObserverArticle> + </IntersectionObserverArticleContainer> ))} {loadMore} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index b8617018d..78177c84d 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -12,7 +12,7 @@ import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components'; +import { MediaGallery, Video } from '../features/ui/util/async-components'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -91,6 +91,10 @@ export default class Status extends ImmutablePureComponent { return <div className='media-spoiler-video' style={{ height: '110px' }} />; } + handleOpenVideo = startTime => { + this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); + } + render () { let media = null; let statusAvatar; @@ -130,9 +134,18 @@ export default class Status extends ImmutablePureComponent { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + const video = status.getIn(['media_attachments', 0]); + media = ( - <Bundle fetchComponent={VideoPlayer} loading={this.renderLoadingVideoPlayer} > - {Component => <Component media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />} + <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > + {Component => <Component + preview={video.get('preview_url')} + src={video.get('url')} + width={239} + height={110} + sensitive={status.get('sensitive')} + onOpenVideo={this.handleOpenVideo} + />} </Bundle> ); } else { diff --git a/app/javascript/mastodon/containers/card_container.js b/app/javascript/mastodon/containers/card_container.js new file mode 100644 index 000000000..11b9f88d4 --- /dev/null +++ b/app/javascript/mastodon/containers/card_container.js @@ -0,0 +1,18 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Card from '../features/status/components/card'; +import { fromJS } from 'immutable'; + +export default class CardContainer extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string, + card: PropTypes.array.isRequired, + }; + + render () { + const { card, ...props } = this.props; + return <Card card={fromJS(card)} {...props} />; + } + +} diff --git a/app/javascript/mastodon/containers/intersection_observer_article_container.js b/app/javascript/mastodon/containers/intersection_observer_article_container.js new file mode 100644 index 000000000..b6f162199 --- /dev/null +++ b/app/javascript/mastodon/containers/intersection_observer_article_container.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import IntersectionObserverArticle from '../components/intersection_observer_article'; +import { setHeight } from '../actions/height_cache'; + +const makeMapStateToProps = (state, props) => ({ + cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]), +}); + +const mapDispatchToProps = (dispatch) => ({ + + onHeightChange (key, id, height) { + dispatch(setHeight(key, id, height)); + }, + +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle); diff --git a/app/javascript/mastodon/containers/media_gallery_container.js b/app/javascript/mastodon/containers/media_gallery_container.js new file mode 100644 index 000000000..812c3d4e5 --- /dev/null +++ b/app/javascript/mastodon/containers/media_gallery_container.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from '../locales'; +import MediaGallery from '../components/media_gallery'; +import { fromJS } from 'immutable'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +export default class MediaGalleryContainer extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + media: PropTypes.array.isRequired, + }; + + handleOpenMedia = () => {} + + render () { + const { locale, media, ...props } = this.props; + + return ( + <IntlProvider locale={locale} messages={messages}> + <MediaGallery + {...props} + media={fromJS(media)} + onOpenMedia={this.handleOpenMedia} + /> + </IntlProvider> + ); + } + +} diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 9dff79b72..e8821223d 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -21,7 +21,7 @@ import { blockAccount, muteAccount, } from '../actions/accounts'; -import { muteStatus, unmuteStatus, deleteStatus, setStatusHeight } from '../actions/statuses'; +import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -141,10 +141,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, - onHeightChange (status, height) { - dispatch(setStatusHeight(status.get('id'), height)); - }, - }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/containers/video_container.js b/app/javascript/mastodon/containers/video_container.js new file mode 100644 index 000000000..2fd353096 --- /dev/null +++ b/app/javascript/mastodon/containers/video_container.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from '../locales'; +import Video from '../features/video'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +export default class VideoContainer extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + }; + + render () { + const { locale, ...props } = this.props; + + return ( + <IntlProvider locale={locale} messages={messages}> + <Video {...props} /> + </IntlProvider> + ); + } + +} diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index a41dfdd1d..865b85b61 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -3,28 +3,48 @@ import Trie from 'substring-trie'; const trie = new Trie(Object.keys(unicodeMapping)); -const emojify = str => { - let rtn = ''; - for (;;) { - let match, i = 0; - while (i < str.length && str[i] !== '<' && !(match = trie.search(str.slice(i)))) { - i += str.codePointAt(i) < 65536 ? 1 : 2; - } - if (i === str.length) - break; - else if (str[i] === '<') { - let tagend = str.indexOf('>', i + 1) + 1; - if (!tagend) - break; - rtn += str.slice(0, tagend); - str = str.slice(tagend); - } else { - const [filename, shortCode] = unicodeMapping[match]; - rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; - str = str.slice(i + match.length); +const emojify = (str, customEmojis = {}) => { + // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.) + // and replacing valid unicode strings + // that _aren't_ within tags with an <img> version. + // The goal is to be the same as an emojione.regUnicode replacement, but faster. + let i = -1; + let insideTag = false; + let insideShortname = false; + let shortnameStartIndex = -1; + let match; + while (++i < str.length) { + const char = str.charAt(i); + if (insideShortname && char === ':') { + const shortname = str.substring(shortnameStartIndex, i + 1); + if (shortname in customEmojis) { + const replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${customEmojis[shortname]}" />`; + str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1); + i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string + } else { + i--; + } + insideShortname = false; + } else if (insideTag && char === '>') { + insideTag = false; + } else if (char === '<') { + insideTag = true; + insideShortname = false; + } else if (!insideTag && char === ':') { + insideShortname = true; + shortnameStartIndex = i; + } else if (!insideTag && (match = trie.search(str.substring(i)))) { + const unicodeStr = match; + if (unicodeStr in unicodeMapping) { + const [filename, shortCode] = unicodeMapping[unicodeStr]; + const alt = unicodeStr; + const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; + str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); + i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string + } } } - return rtn + str; + return str; }; export default emojify; diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js index f0fea1a0e..588a372c6 100644 --- a/app/javascript/mastodon/features/compose/util/counter.js +++ b/app/javascript/mastodon/features/compose/util/counter.js @@ -1,7 +1,9 @@ +import { urlRegex } from './url_regex'; + const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx'; export function countableText(inputText) { return inputText - .replace(/https?:\/\/\S+/g, urlPlaceholder) + .replace(urlRegex, urlPlaceholder) .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2'); }; diff --git a/app/javascript/mastodon/features/compose/util/url_regex.js b/app/javascript/mastodon/features/compose/util/url_regex.js new file mode 100644 index 000000000..e676d1879 --- /dev/null +++ b/app/javascript/mastodon/features/compose/util/url_regex.js @@ -0,0 +1,196 @@ +const regexen = {}; + +const regexSupplant = function(regex, flags) { + flags = flags || ''; + if (typeof regex !== 'string') { + if (regex.global && flags.indexOf('g') < 0) { + flags += 'g'; + } + if (regex.ignoreCase && flags.indexOf('i') < 0) { + flags += 'i'; + } + if (regex.multiline && flags.indexOf('m') < 0) { + flags += 'm'; + } + + regex = regex.source; + } + return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { + var newRegex = regexen[name] || ''; + if (typeof newRegex !== 'string') { + newRegex = newRegex.source; + } + return newRegex; + }), flags); +}; + +const stringSupplant = function(str, values) { + return str.replace(/#\{(\w+)\}/g, function(match, name) { + return values[name] || ''; + }); +}; + +export const urlRegex = (function() { + regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/; + regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/; + regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/; + regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@@$###{invalid_chars_group}]|^)/); + regexen.invalidDomainChars = stringSupplant('#{punct}#{spaces_group}#{invalid_chars_group}', regexen); + regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/); + regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/); + regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/); + regexen.validGTLD = regexSupplant(RegExp( + '(?:(?:' + + '삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' + + '政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' + + 'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' + + 'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' + + 'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' + + 'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' + + 'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' + + 'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' + + 'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' + + 'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' + + 'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' + + 'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' + + 'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' + + 'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' + + 'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' + + 'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' + + 'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' + + 'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' + + 'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' + + 'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' + + 'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' + + 'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' + + 'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' + + 'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' + + 'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' + + 'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' + + 'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' + + 'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' + + 'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' + + 'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' + + 'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' + + 'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' + + 'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' + + 'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' + + 'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' + + 'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' + + 'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' + + 'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' + + 'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' + + 'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' + + 'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' + + 'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' + + 'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' + + 'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' + + 'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' + + 'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' + + 'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' + + 'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' + + 'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' + + 'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' + + 'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' + + 'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' + + 'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' + + 'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' + + 'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' + + 'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' + + 'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' + + 'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' + + 'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' + + 'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' + + 'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' + + 'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' + + 'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' + + 'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' + + 'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' + + 'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' + + 'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' + + 'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' + + 'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' + + 'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' + + 'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' + + 'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' + + 'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' + + 'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' + + 'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' + + 'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' + + 'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' + + 'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' + + 'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' + + 'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' + + 'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' + + 'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' + + 'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' + + 'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' + + 'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' + + 'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' + + 'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' + + 'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' + + ')(?=[^0-9a-zA-Z@]|$))')); + regexen.validCCTLD = regexSupplant(RegExp( + '(?:(?:' + + '한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' + + 'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' + + 'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' + + 'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' + + 'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' + + 're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' + + 'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' + + 'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' + + 'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' + + 'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' + + 'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' + + ')(?=[^0-9a-zA-Z@]|$))')); + regexen.validPunycode = /(?:xn--[0-9a-z]+)/; + regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/; + regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/); + regexen.validPortNumber = /[0-9]+/; + regexen.pd = /\u002d\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a\u2e40\u301c\u3030\u30a0\ufe31\ufe58\ufe63\uff0d/; + regexen.validGeneralUrlPathChars = regexSupplant(/[^#{spaces_group}\(\)\?]/i); + // Allow URL paths to contain up to two nested levels of balanced parens + // 1. Used in Wikipedia URLs like /Primer_(film) + // 2. Used in IIS sessions like /S(dfd346)/ + // 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/ + regexen.validUrlBalancedParens = regexSupplant( + '\\(' + + '(?:' + + '#{validGeneralUrlPathChars}+' + + '|' + + // allow one nested level of balanced parentheses + '(?:' + + '#{validGeneralUrlPathChars}*' + + '\\(' + + '#{validGeneralUrlPathChars}+' + + '\\)' + + '#{validGeneralUrlPathChars}*' + + ')' + + ')' + + '\\)' + , 'i'); + // Valid end-of-path chracters (so /foo. does not gobble the period). + // 1. Allow =&# for empty URL parameters and other URL-join artifacts + regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i); + // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/ + regexen.validUrlPath = regexSupplant('(?:' + + '(?:' + + '#{validGeneralUrlPathChars}*' + + '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' + + '#{validUrlPathEndingChars}'+ + ')|(?:@#{validGeneralUrlPathChars}+\/)'+ + ')', 'i'); + regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i; + regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i; + regexen.validUrl = regexSupplant( + '(' + // $1 URL + '(https?:\\/\\/)' + // $2 Protocol + '(#{validDomain})' + // $3 Domain(s) + '(?::(#{validPortNumber}))?' + // $4 Port number (optional) + '(\\/#{validUrlPath}*)?' + // $5 URL Path + '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $6 Query String + ')' + , 'gi'); + return regexen.validUrl; +}()); diff --git a/app/javascript/mastodon/features/standalone/compose/index.js b/app/javascript/mastodon/features/standalone/compose/index.js index 96d07fefb..0d764575f 100644 --- a/app/javascript/mastodon/features/standalone/compose/index.js +++ b/app/javascript/mastodon/features/standalone/compose/index.js @@ -2,6 +2,7 @@ import React from 'react'; import ComposeFormContainer from '../../compose/containers/compose_form_container'; import NotificationsContainer from '../../ui/containers/notifications_container'; import LoadingBarContainer from '../../ui/containers/loading_bar_container'; +import ModalContainer from '../../ui/containers/modal_container'; export default class Compose extends React.PureComponent { @@ -10,6 +11,7 @@ export default class Compose extends React.PureComponent { <div> <ComposeFormContainer /> <NotificationsContainer /> + <ModalContainer /> <LoadingBarContainer className='loading-bar' /> </div> ); diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 6b13e15cc..41c4300d3 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import punycode from 'punycode'; import classnames from 'classnames'; @@ -22,10 +23,15 @@ export default class Card extends React.PureComponent { static propTypes = { card: ImmutablePropTypes.map, + maxDescription: PropTypes.number, + }; + + static defaultProps = { + maxDescription: 50, }; renderLink () { - const { card } = this.props; + const { card, maxDescription } = this.props; let image = ''; let provider = card.get('provider_name'); @@ -52,7 +58,7 @@ export default class Card extends React.PureComponent { <div className='status-card__content'> <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> - <p className='status-card__description'>{(card.get('description') || '').substring(0, 50)}</p> + <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p> <span className='status-card__host'>{provider}</span> </div> </a> diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index b4979c603..8cd5abd3f 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -11,6 +11,7 @@ import Link from 'react-router-dom/Link'; import { FormattedDate, FormattedNumber } from 'react-intl'; import CardContainer from '../containers/card_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Video from '../../video'; import VisibilityIcon from '../../../../glitch/components/status/visibility_icon'; export default class DetailedStatus extends ImmutablePureComponent { @@ -36,6 +37,10 @@ export default class DetailedStatus extends ImmutablePureComponent { e.stopPropagation(); } + handleOpenVideo = startTime => { + this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); + } + render () { const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; const { settings } = this.props; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 539af8ce3..5610095b9 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -78,7 +78,7 @@ export default class ColumnsArea extends ImmutablePureComponent { handleChildrenContentChange() { if (!this.props.singleColumn) { - scrollRight(this.node, this.node.scrollWidth - window.innerWidth); + this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth); } } diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js index 9a9a49dfb..867c73ed5 100644 --- a/app/javascript/mastodon/features/ui/components/video_modal.js +++ b/app/javascript/mastodon/features/ui/components/video_modal.js @@ -1,35 +1,29 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ExtendedVideoPlayer from '../../../components/extended_video_player'; -import { defineMessages, injectIntl } from 'react-intl'; -import IconButton from '../../../components/icon_button'; +import Video from '../../video'; import ImmutablePureComponent from 'react-immutable-pure-component'; -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, -}); - -@injectIntl export default class VideoModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, time: PropTypes.number, onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, }; render () { - const { media, intl, time, onClose } = this.props; - - const url = media.get('url'); + const { media, time, onClose } = this.props; return ( <div className='modal-root__modal media-modal'> <div> - <div className='media-modal__close'><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div> - <ExtendedVideoPlayer src={url} muted={false} controls time={time} /> + <Video + preview={media.get('preview_url')} + src={media.get('url')} + startTime={time} + onCloseVideo={onClose} + /> </div> </div> ); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 7d12210bb..3732d301f 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -11,7 +11,7 @@ import { debounce } from 'lodash'; import { uploadCompose } from '../../actions/compose'; import { refreshHomeTimeline } from '../../actions/timelines'; import { refreshNotifications } from '../../actions/notifications'; -import { clearStatusesHeight } from '../../actions/statuses'; +import { clearHeight } from '../../actions/height_cache'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -77,7 +77,7 @@ export default class UI extends React.PureComponent { handleResize = debounce(() => { // The cached heights are no longer accurate, invalidate - this.props.dispatch(clearStatusesHeight()); + this.props.dispatch(clearHeight()); this.setState({ width: window.innerWidth }); }, 500, { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 2f5c52e9e..ddb7e32c9 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -109,6 +109,10 @@ export function VideoPlayer () { return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player'); } +export function Video () { + return import(/* webpackChunkName: "features/video" */'../../video'); +} + export function EmbedModal () { return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal'); } diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js new file mode 100644 index 000000000..f228e434b --- /dev/null +++ b/app/javascript/mastodon/features/video/index.js @@ -0,0 +1,304 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { throttle } from 'lodash'; +import classNames from 'classnames'; + +const messages = defineMessages({ + play: { id: 'video.play', defaultMessage: 'Play' }, + pause: { id: 'video.pause', defaultMessage: 'Pause' }, + mute: { id: 'video.mute', defaultMessage: 'Mute sound' }, + unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, + hide: { id: 'video.hide', defaultMessage: 'Hide video' }, + expand: { id: 'video.expand', defaultMessage: 'Expand video' }, + close: { id: 'video.close', defaultMessage: 'Close video' }, + fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' }, + exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' }, +}); + +const findElementPosition = el => { + let box; + + if (el.getBoundingClientRect && el.parentNode) { + box = el.getBoundingClientRect(); + } + + if (!box) { + return { + left: 0, + top: 0, + }; + } + + const docEl = document.documentElement; + const body = document.body; + + const clientLeft = docEl.clientLeft || body.clientLeft || 0; + const scrollLeft = window.pageXOffset || body.scrollLeft; + const left = (box.left + scrollLeft) - clientLeft; + + const clientTop = docEl.clientTop || body.clientTop || 0; + const scrollTop = window.pageYOffset || body.scrollTop; + const top = (box.top + scrollTop) - clientTop; + + return { + left: Math.round(left), + top: Math.round(top), + }; +}; + +const getPointerPosition = (el, event) => { + const position = {}; + const box = findElementPosition(el); + const boxW = el.offsetWidth; + const boxH = el.offsetHeight; + const boxY = box.top; + const boxX = box.left; + + let pageY = event.pageY; + let pageX = event.pageX; + + if (event.changedTouches) { + pageX = event.changedTouches[0].pageX; + pageY = event.changedTouches[0].pageY; + } + + position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH)); + position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW)); + + return position; +}; + +const isFullscreen = () => document.fullscreenElement || + document.webkitFullscreenElement || + document.mozFullScreenElement || + document.msFullscreenElement; + +const exitFullscreen = () => { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } +}; + +const requestFullscreen = el => { + if (el.requestFullscreen) { + el.requestFullscreen(); + } else if (el.webkitRequestFullscreen) { + el.webkitRequestFullscreen(); + } else if (el.mozRequestFullScreen) { + el.mozRequestFullScreen(); + } else if (el.msRequestFullscreen) { + el.msRequestFullscreen(); + } +}; + +@injectIntl +export default class Video extends React.PureComponent { + + static propTypes = { + preview: PropTypes.string, + src: PropTypes.string.isRequired, + width: PropTypes.number, + height: PropTypes.number, + sensitive: PropTypes.bool, + startTime: PropTypes.number, + onOpenVideo: PropTypes.func, + onCloseVideo: PropTypes.func, + intl: PropTypes.object.isRequired, + }; + + state = { + progress: 0, + paused: true, + dragging: false, + fullscreen: false, + hovered: false, + muted: false, + revealed: !this.props.sensitive, + }; + + setPlayerRef = c => { + this.player = c; + } + + setVideoRef = c => { + this.video = c; + } + + setSeekRef = c => { + this.seek = c; + } + + handlePlay = () => { + this.setState({ paused: false }); + } + + handlePause = () => { + this.setState({ paused: true }); + } + + handleTimeUpdate = () => { + this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) }); + } + + handleMouseDown = e => { + document.addEventListener('mousemove', this.handleMouseMove, true); + document.addEventListener('mouseup', this.handleMouseUp, true); + document.addEventListener('touchmove', this.handleMouseMove, true); + document.addEventListener('touchend', this.handleMouseUp, true); + + this.setState({ dragging: true }); + this.video.pause(); + this.handleMouseMove(e); + } + + handleMouseUp = () => { + document.removeEventListener('mousemove', this.handleMouseMove, true); + document.removeEventListener('mouseup', this.handleMouseUp, true); + document.removeEventListener('touchmove', this.handleMouseMove, true); + document.removeEventListener('touchend', this.handleMouseUp, true); + + this.setState({ dragging: false }); + this.video.play(); + } + + handleMouseMove = throttle(e => { + const { x } = getPointerPosition(this.seek, e); + this.video.currentTime = this.video.duration * x; + this.setState({ progress: x * 100 }); + }, 60); + + togglePlay = () => { + if (this.state.paused) { + this.video.play(); + } else { + this.video.pause(); + } + } + + toggleFullscreen = () => { + if (isFullscreen()) { + exitFullscreen(); + } else { + requestFullscreen(this.player); + } + } + + componentDidMount () { + document.addEventListener('fullscreenchange', this.handleFullscreenChange, true); + document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); + document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); + document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); + } + + componentWillUnmount () { + document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true); + document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); + document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true); + document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true); + } + + handleFullscreenChange = () => { + this.setState({ fullscreen: isFullscreen() }); + } + + handleMouseEnter = () => { + this.setState({ hovered: true }); + } + + handleMouseLeave = () => { + this.setState({ hovered: false }); + } + + toggleMute = () => { + this.video.muted = !this.video.muted; + this.setState({ muted: this.video.muted }); + } + + toggleReveal = () => { + if (this.state.revealed) { + this.video.pause(); + } + + this.setState({ revealed: !this.state.revealed }); + } + + handleLoadedData = () => { + if (this.props.startTime) { + this.video.currentTime = this.props.startTime; + this.video.play(); + } + } + + handleOpenVideo = () => { + this.video.pause(); + this.props.onOpenVideo(this.video.currentTime); + } + + handleCloseVideo = () => { + this.video.pause(); + this.props.onCloseVideo(); + } + + render () { + const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl } = this.props; + const { progress, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; + + return ( + <div className={classNames('video-player', { inactive: !revealed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + <video + ref={this.setVideoRef} + src={src} + poster={preview} + preload={!!startTime} + loop + role='button' + tabIndex='0' + width={width} + height={height} + onClick={this.togglePlay} + onPlay={this.handlePlay} + onPause={this.handlePause} + onTimeUpdate={this.handleTimeUpdate} + onLoadedData={this.handleLoadedData} + /> + + <button className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}> + <span className='video-player__spoiler__title'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> + <span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> + </button> + + <div className={classNames('video-player__controls', { active: paused || hovered })}> + <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> + <div className='video-player__seek__progress' style={{ width: `${progress}%` }} /> + + <span + className={classNames('video-player__seek__handle', { active: dragging })} + tabIndex='0' + style={{ left: `${progress}%` }} + /> + </div> + + <div className='video-player__buttons left'> + <button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button> + <button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button> + {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} + </div> + + <div className='video-player__buttons right'> + {(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>} + {onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-times' /></button>} + <button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 2ceb6eb9a..3a6fa2874 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -33,6 +33,7 @@ "column.home": "الرئيسية", "column.mutes": "الحسابات المكتومة", "column.notifications": "الإشعارات", + "column.pins": "Pinned toot", "column.public": "الخيط العام الموحد", "column_back_button.label": "العودة", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "معلومات إضافية", "navigation_bar.logout": "خروج", "navigation_bar.mutes": "الحسابات المكتومة", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "التفضيلات", "navigation_bar.public_timeline": "الخيط العام الموحد", "notification.favourite": "{name} أعجب بمنشورك", @@ -193,6 +195,15 @@ "upload_button.label": "إضافة وسائط", "upload_form.undo": "إلغاء", "upload_progress.label": "يرفع...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "وسّع الفيديو", "video_player.toggle_sound": "تبديل الصوت", "video_player.toggle_visible": "إظهار / إخفاء الفيديو", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 183ba2673..9afe2d038 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -33,6 +33,7 @@ "column.home": "Начало", "column.mutes": "Muted users", "column.notifications": "Известия", + "column.pins": "Pinned toot", "column.public": "Публичен канал", "column_back_button.label": "Назад", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Extended information", "navigation_bar.logout": "Излизане", "navigation_bar.mutes": "Muted users", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Предпочитания", "navigation_bar.public_timeline": "Публичен канал", "notification.favourite": "{name} хареса твоята публикация", @@ -193,6 +195,15 @@ "upload_button.label": "Добави медия", "upload_form.undo": "Отмяна", "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Expand video", "video_player.toggle_sound": "Звук", "video_player.toggle_visible": "Toggle visibility", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 0e3d2bc18..7d45b4d6b 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -33,6 +33,7 @@ "column.home": "Inici", "column.mutes": "Usuaris silenciats", "column.notifications": "Notificacions", + "column.pins": "Pinned toot", "column.public": "Línia de temps federada", "column_back_button.label": "Enrere", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Informació addicional", "navigation_bar.logout": "Tancar sessió", "navigation_bar.mutes": "Usuaris silenciats", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferències", "navigation_bar.public_timeline": "Línia de temps federada", "notification.favourite": "{name} ha afavorit el teu estat", @@ -193,6 +195,15 @@ "upload_button.label": "Afegir multimèdia", "upload_form.undo": "Desfer", "upload_progress.label": "Pujant...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Ampliar el vídeo", "video_player.toggle_sound": "Alternar so", "video_player.toggle_visible": "Alternar visibilitat", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 3133238cd..712c635c8 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -33,6 +33,7 @@ "column.home": "Startseite", "column.mutes": "Stummgeschaltete Profile", "column.notifications": "Mitteilungen", + "column.pins": "Pinned toot", "column.public": "Gesamtes bekanntes Netz", "column_back_button.label": "Zurück", "column_header.hide_settings": "Einstellungen verbergen", @@ -109,6 +110,7 @@ "navigation_bar.info": "Erweiterte Informationen", "navigation_bar.logout": "Abmelden", "navigation_bar.mutes": "Stummgeschaltete Profile", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Einstellungen", "navigation_bar.public_timeline": "Föderierte Zeitleiste", "notification.favourite": "{name} favorisierte deinen Status", @@ -193,6 +195,15 @@ "upload_button.label": "Mediendatei hinzufügen", "upload_form.undo": "Entfernen", "upload_progress.label": "Lade hoch…", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Videoanzeige vergrößern", "video_player.toggle_sound": "Ton umschalten", "video_player.toggle_visible": "Sichtbarkeit umschalten", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 89f74a56b..3c19ad7dc 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -813,6 +813,10 @@ "id": "navigation_bar.info" }, { + "defaultMessage": "Pinned toots", + "id": "navigation_bar.pins" + }, + { "defaultMessage": "FAQ", "id": "getting_started.faq" }, @@ -995,6 +999,15 @@ { "descriptors": [ { + "defaultMessage": "Pinned toot", + "id": "column.pins" + } + ], + "path": "app/javascript/mastodon/features/pinned_statuses/index.json" + }, + { + "descriptors": [ + { "defaultMessage": "Federated timeline", "id": "column.public" }, @@ -1326,5 +1339,54 @@ } ], "path": "app/javascript/mastodon/features/ui/components/video_modal.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Play", + "id": "video.play" + }, + { + "defaultMessage": "Pause", + "id": "video.pause" + }, + { + "defaultMessage": "Mute sound", + "id": "video.mute" + }, + { + "defaultMessage": "Unmute sound", + "id": "video.unmute" + }, + { + "defaultMessage": "Hide video", + "id": "video.hide" + }, + { + "defaultMessage": "Expand video", + "id": "video.expand" + }, + { + "defaultMessage": "Close video", + "id": "video.close" + }, + { + "defaultMessage": "Full screen", + "id": "video.fullscreen" + }, + { + "defaultMessage": "Exit full screen", + "id": "video.exit_fullscreen" + }, + { + "defaultMessage": "Sensitive content", + "id": "status.sensitive_warning" + }, + { + "defaultMessage": "Click to view", + "id": "status.sensitive_toggle" + } + ], + "path": "app/javascript/mastodon/features/video/index.json" } ] diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f42851f45..436079aeb 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -33,8 +33,8 @@ "column.home": "Home", "column.mutes": "Muted users", "column.notifications": "Notifications", - "column.public": "Federated timeline", "column.pins": "Pinned toots", + "column.public": "Federated timeline", "column_back_button.label": "Back", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", @@ -110,9 +110,9 @@ "navigation_bar.info": "About this instance", "navigation_bar.logout": "Logout", "navigation_bar.mutes": "Muted users", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferences", "navigation_bar.public_timeline": "Federated timeline", - "navigation_bar.pins": "Pinned toots", "notification.favourite": "{name} favourited your status", "notification.follow": "{name} followed you", "notification.mention": "{name} mentioned you", @@ -195,6 +195,15 @@ "upload_button.label": "Add media", "upload_form.undo": "Undo", "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Expand video", "video_player.toggle_sound": "Toggle sound", "video_player.toggle_visible": "Toggle visibility", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index d828d0858..945fcd8e0 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -33,6 +33,7 @@ "column.home": "Hejmo", "column.mutes": "Muted users", "column.notifications": "Sciigoj", + "column.pins": "Pinned toot", "column.public": "Fratara tempolinio", "column_back_button.label": "Reveni", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Extended information", "navigation_bar.logout": "Elsaluti", "navigation_bar.mutes": "Muted users", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferoj", "navigation_bar.public_timeline": "Fratara tempolinio", "notification.favourite": "{name} favoris vian mesaĝon", @@ -193,6 +195,15 @@ "upload_button.label": "Aldoni enhavaĵon", "upload_form.undo": "Malfari", "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Expand video", "video_player.toggle_sound": "Aktivigi sonojn", "video_player.toggle_visible": "Toggle visibility", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index d35eb84e7..5182b5094 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -1,106 +1,107 @@ { "account.block": "Bloquear", - "account.block_domain": "Hide everything from {domain}", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.block_domain": "Ocultar todo de {domain}", + "account.disclaimer_full": "La siguiente información del usuario puede estar incompleta.", "account.edit_profile": "Editar perfil", "account.follow": "Seguir", "account.followers": "Seguidores", - "account.follows": "Seguir", + "account.follows": "Sigue", "account.follows_you": "Te sigue", "account.media": "Media", - "account.mention": "Mencionar", - "account.mute": "Silenciar", + "account.mention": "Mencionar a @{name}", + "account.mute": "Silenciar a @{name}", "account.posts": "Publicaciones", - "account.report": "Report @{name}", + "account.report": "Reportar a @{name}", "account.requested": "Esperando aprobación", - "account.share": "Share @{name}'s profile", - "account.unblock": "Desbloquear", - "account.unblock_domain": "Unhide {domain}", + "account.share": "Compartir el perfil de @{name}", + "account.unblock": "Desbloquear a @{name}", + "account.unblock_domain": "Mostrar a {domain}", "account.unfollow": "Dejar de seguir", - "account.unmute": "Unmute @{name}", - "account.view_full_profile": "View full profile", - "boost_modal.combo": "You can press {combo} to skip this next time", - "bundle_column_error.body": "Something went wrong while loading this component.", - "bundle_column_error.retry": "Try again", - "bundle_column_error.title": "Network error", - "bundle_modal_error.close": "Close", - "bundle_modal_error.message": "Something went wrong while loading this component.", - "bundle_modal_error.retry": "Try again", + "account.unmute": "Dejar de silenciar a @{name}", + "account.view_full_profile": "Ver perfil completo", + "boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez", + "bundle_column_error.body": "Algo salió mal al cargar este componente.", + "bundle_column_error.retry": "Inténtalo de nuevo", + "bundle_column_error.title": "Error de red", + "bundle_modal_error.close": "Cerrar", + "bundle_modal_error.message": "Algo salió mal al cargar este componente.", + "bundle_modal_error.retry": "Inténtalo de nuevo", "column.blocks": "Usuarios bloqueados", - "column.community": "Historia local", + "column.community": "Línea de tiempo local", "column.favourites": "Favoritos", - "column.follow_requests": "Solicitudes para seguirte", + "column.follow_requests": "Solicitudes de seguimiento", "column.home": "Inicio", "column.mutes": "Usuarios silenciados", "column.notifications": "Notificaciones", + "column.pins": "Toot fijado", "column.public": "Historia federada", "column_back_button.label": "Atrás", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", - "column_header.pin": "Pin", - "column_header.show_settings": "Show settings", - "column_header.unpin": "Unpin", - "column_subheading.navigation": "Navigation", - "column_subheading.settings": "Settings", - "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", - "compose_form.lock_disclaimer.lock": "locked", + "column_header.hide_settings": "Ocultar ajustes", + "column_header.moveLeft_settings": "Mover columna a la izquierda", + "column_header.moveRight_settings": "Mover columna a la derecha", + "column_header.pin": "Fijar", + "column_header.show_settings": "Mostrar ajustes", + "column_header.unpin": "Dejar de fijar", + "column_subheading.navigation": "Navegación", + "column_subheading.settings": "Ajustes", + "compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.", + "compose_form.lock_disclaimer.lock": "bloqueado", "compose_form.placeholder": "¿En qué estás pensando?", - "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.", + "compose_form.privacy_disclaimer": "Tu toot privado será enviado a usuario/s mencionados de {domains}. ¿Confías en {domainsCount, plural, one {ese servidor} other {esos servidores}}? La privacidad del toot funcionará solamente en instancias de Mastodon. Si {domains} {domainsCount, plural, one {no es una instancia de Mastodon} other {no son instancias de Mastodon}}, no habrá indicación de que tu toot es privado, y puede hacerse visible a remitentes inesperados.", "compose_form.publish": "Tootear", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "Marcar contenido como sensible", - "compose_form.spoiler": "Ocultar texto tras advertencia", + "compose_form.spoiler": "Ocultar texto tras una advertencia", "compose_form.spoiler_placeholder": "Advertencia de contenido", - "confirmation_modal.cancel": "Cancel", - "confirmations.block.confirm": "Block", - "confirmations.block.message": "Are you sure you want to block {name}?", - "confirmations.delete.confirm": "Delete", - "confirmations.delete.message": "Are you sure you want to delete this status?", - "confirmations.domain_block.confirm": "Hide entire domain", - "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", - "confirmations.mute.confirm": "Mute", - "confirmations.mute.message": "Are you sure you want to mute {name}?", - "confirmations.unfollow.confirm": "Unfollow", - "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.preview": "Here is what it will look like:", - "emoji_button.activity": "Activity", - "emoji_button.flags": "Flags", - "emoji_button.food": "Food & Drink", + "confirmation_modal.cancel": "Cancelar", + "confirmations.block.confirm": "Bloquear", + "confirmations.block.message": "¿Estás seguro de que quieres bloquear a {name}?", + "confirmations.delete.confirm": "Eliminar", + "confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?", + "confirmations.domain_block.confirm": "Ocultar dominio entero", + "confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio entero? En algunos casos es preferible bloquear o silenciar objetivos determinados.", + "confirmations.mute.confirm": "Silenciar", + "confirmations.mute.message": "¿Estás seguro de que quieres silenciar a {name}?", + "confirmations.unfollow.confirm": "Dejar de seguir", + "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?", + "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.", + "embed.preview": "Así es como se verá:", + "emoji_button.activity": "Actividad", + "emoji_button.flags": "Marcas", + "emoji_button.food": "Comida y bebida", "emoji_button.label": "Insertar emoji", - "emoji_button.nature": "Nature", - "emoji_button.objects": "Objects", - "emoji_button.people": "People", - "emoji_button.search": "Search...", - "emoji_button.symbols": "Symbols", - "emoji_button.travel": "Travel & Places", - "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", - "empty_column.hashtag": "There is nothing in this hashtag yet.", - "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", - "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.", - "empty_column.home.public_timeline": "the public timeline", - "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", - "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", - "follow_request.authorize": "Authorize", - "follow_request.reject": "Reject", - "getting_started.appsshort": "Apps", + "emoji_button.nature": "Naturaleza", + "emoji_button.objects": "Objetos", + "emoji_button.people": "Gente", + "emoji_button.search": "Buscar…", + "emoji_button.symbols": "Símbolos", + "emoji_button.travel": "Viajes y lugares", + "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!", + "empty_column.hashtag": "No hay nada en este hashtag aún.", + "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.", + "empty_column.home.inactivity": "Tus notificaciones están vacías. Si has estado inactivo por un tiempo, se regenerará para ti pronto.", + "empty_column.home.public_timeline": "la línea de tiempo pública", + "empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.", + "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.", + "follow_request.authorize": "Autorizar", + "follow_request.reject": "Rechazar", + "getting_started.appsshort": "Aplicaciones", "getting_started.faq": "FAQ", "getting_started.heading": "Primeros pasos", "getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}.", - "getting_started.userguide": "User Guide", - "home.column_settings.advanced": "Advanced", - "home.column_settings.basic": "Basic", - "home.column_settings.filter_regex": "Filter out by regular expressions", - "home.column_settings.show_reblogs": "Show boosts", - "home.column_settings.show_replies": "Show replies", - "home.settings": "Column settings", + "getting_started.userguide": "Guía de usuario", + "home.column_settings.advanced": "Avanzado", + "home.column_settings.basic": "Básico", + "home.column_settings.filter_regex": "Filtrar con expresiones regulares", + "home.column_settings.show_reblogs": "Mostrar retoots", + "home.column_settings.show_replies": "Mostrar respuestas", + "home.settings": "Ajustes de columna", "lightbox.close": "Cerrar", - "lightbox.next": "Next", - "lightbox.previous": "Previous", - "loading_indicator.label": "Cargando...", - "media_gallery.toggle_visible": "Toggle visibility", - "missing_indicator.label": "Not found", + "lightbox.next": "Siguiente", + "lightbox.previous": "Anterior", + "loading_indicator.label": "Cargando…", + "media_gallery.toggle_visible": "Cambiar visibilidad", + "missing_indicator.label": "No encontrado", "navigation_bar.blocks": "Usuarios bloqueados", "navigation_bar.community_timeline": "Historia local", "navigation_bar.edit_profile": "Editar perfil", @@ -109,43 +110,44 @@ "navigation_bar.info": "Información adicional", "navigation_bar.logout": "Cerrar sesión", "navigation_bar.mutes": "Usuarios silenciados", + "navigation_bar.pins": "Toots fijados", "navigation_bar.preferences": "Preferencias", "navigation_bar.public_timeline": "Historia federada", "notification.favourite": "{name} marcó tu estado como favorito", "notification.follow": "{name} te empezó a seguir", "notification.mention": "{name} te ha mencionado", "notification.reblog": "{name} ha retooteado tu estado", - "notifications.clear": "Clear notifications", - "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.clear": "Limpiar notificaciones", + "notifications.clear_confirmation": "¿Seguro que quieres limpiar permanentemente todas tus notificaciones?", "notifications.column_settings.alert": "Notificaciones de escritorio", "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.mention": "Menciones:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.push_meta": "This device", + "notifications.column_settings.push": "Notificaciones push:", + "notifications.column_settings.push_meta": "Este dispositivo:", "notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.show": "Mostrar en columna", - "notifications.column_settings.sound": "Play sound", - "onboarding.done": "Done", - "onboarding.next": "Next", - "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", - "onboarding.page_four.home": "The home timeline shows posts from people you follow.", - "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", - "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", - "onboarding.page_one.welcome": "Welcome to Mastodon!", - "onboarding.page_six.admin": "Your instance's admin is {admin}.", - "onboarding.page_six.almost_done": "Almost done...", - "onboarding.page_six.appetoot": "Bon Appetoot!", - "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", - "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", - "onboarding.page_six.guidelines": "community guidelines", - "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", - "onboarding.page_six.various_app": "mobile apps", - "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", - "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", - "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", - "onboarding.skip": "Skip", + "notifications.column_settings.sound": "Reproducir sonido", + "onboarding.done": "Listo", + "onboarding.next": "Siguiente", + "onboarding.page_five.public_timelines": "La línea de tiempo local muestra toots públicos de todos en {domain}. La línea de tiempo federada muestra toots públicos de cualquiera a quien la gente de {domain} siga. Estas son las líneas de tiempo públicas, una buena forma de conocer gente nueva.", + "onboarding.page_four.home": "La línea de tiempo principal muestra toots de gente que sigues.", + "onboarding.page_four.notifications": "Las notificaciones se muestran cuando alguien interactúa contigo.", + "onboarding.page_one.federation": "Mastodon es una red de servidores federados que conforman una red social aún más grande. Llamamos a estos servidores instancias.", + "onboarding.page_one.handle": "Estás en {domain}, así que tu nombre de usuario completo es {handle}", + "onboarding.page_one.welcome": "¡Bienvenido a Mastodon!", + "onboarding.page_six.admin": "El administrador de tu instancia es {admin}.", + "onboarding.page_six.almost_done": "Ya casi…", + "onboarding.page_six.appetoot": "¡Bon Appetoot!", + "onboarding.page_six.apps_available": "Hay {apps} disponibles para iOS, Android y otras plataformas.", + "onboarding.page_six.github": "Mastodon es software libre. Puedes reportar errores, pedir funciones nuevas, o contribuir al código en {github}.", + "onboarding.page_six.guidelines": "guías de la comunidad", + "onboarding.page_six.read_guidelines": "¡Por favor lee las {guidelines} de {domain}!", + "onboarding.page_six.various_app": "aplicaciones móviles", + "onboarding.page_three.profile": "Edita tu perfil para cambiar tu avatar, biografía y nombre de cabecera. Ahí, también encontrarás otros ajustes.", + "onboarding.page_three.search": "Usa la barra de búsqueda y revisa hashtags, como {illustration} y {introductions}. Para ver a alguien que no es de tu propia instancia, usa su nombre de usuario completo.", + "onboarding.page_two.compose": "Escribe toots en la columna de redacción. Puedes subir imágenes, cambiar ajustes de privacidad, y añadir advertencias de contenido con los siguientes íconos.", + "onboarding.skip": "Saltar", "privacy.change": "Ajustar privacidad", "privacy.direct.long": "Sólo mostrar a los usuarios mencionados", "privacy.direct.short": "Directo", @@ -156,45 +158,54 @@ "privacy.unlisted.long": "No mostrar en la historia federada", "privacy.unlisted.short": "Sin federar", "reply_indicator.cancel": "Cancelar", - "report.placeholder": "Additional comments", - "report.submit": "Submit", - "report.target": "Reporting", + "report.placeholder": "Comentarios adicionales", + "report.submit": "Publicar", + "report.target": "Reportando", "search.placeholder": "Buscar", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "standalone.public_title": "A look inside...", - "status.cannot_reblog": "This post cannot be boosted", + "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", + "standalone.public_title": "Un pequeño vistazo...", + "status.cannot_reblog": "Este toot no puede retootearse", "status.delete": "Borrar", - "status.embed": "Embed", + "status.embed": "Incrustado", "status.favourite": "Favorito", - "status.load_more": "Load more", - "status.media_hidden": "Media hidden", + "status.load_more": "Cargar más", + "status.media_hidden": "Contenido multimedia oculto", "status.mention": "Mencionar", - "status.mute_conversation": "Mute conversation", + "status.mute_conversation": "Silenciar conversación", "status.open": "Expandir estado", - "status.pin": "Pin on profile", - "status.reblog": "Retoot", + "status.pin": "Fijar", + "status.reblog": "Retootear", "status.reblogged_by": "Retooteado por {name}", "status.reply": "Responder", - "status.replyAll": "Reply to thread", + "status.replyAll": "Responder al hilo", "status.report": "Reportar", - "status.sensitive_toggle": "Click para ver", + "status.sensitive_toggle": "Haz clic para ver", "status.sensitive_warning": "Contenido sensible", - "status.share": "Share", + "status.share": "Compartir", "status.show_less": "Mostrar menos", "status.show_more": "Mostrar más", - "status.unmute_conversation": "Unmute conversation", - "status.unpin": "Unpin from profile", + "status.unmute_conversation": "Dejar de silenciar conversación", + "status.unpin": "Dejar de fijar", "tabs_bar.compose": "Redactar", - "tabs_bar.federated_timeline": "Federated", + "tabs_bar.federated_timeline": "Federado", "tabs_bar.home": "Inicio", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificaciones", - "upload_area.title": "Drag & drop to upload", + "upload_area.title": "Arrastra y suelta para subir", "upload_button.label": "Subir multimedia", "upload_form.undo": "Deshacer", - "upload_progress.label": "Uploading...", - "video_player.expand": "Expand video", - "video_player.toggle_sound": "Act/Desac. sonido", - "video_player.toggle_visible": "Toggle visibility", - "video_player.video_error": "Video could not be played" + "upload_progress.label": "Subiendo…", + "video.close": "Cerrar video", + "video.exit_fullscreen": "Salir de pantalla completa", + "video.expand": "Expandir vídeo", + "video.fullscreen": "Pantalla completa", + "video.hide": "Ocultar vídeo", + "video.mute": "Silenciar sonido", + "video.pause": "Pausar", + "video.play": "Reproducir", + "video.unmute": "Dejar de silenciar sonido", + "video_player.expand": "Expandir vídeo", + "video_player.toggle_sound": "Activar/Desactivar sonido", + "video_player.toggle_visible": "Cambiar visibilidad", + "video_player.video_error": "No se pudo reproducir el vídeo" } diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index d05b26eb9..23f4a41d6 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -33,6 +33,7 @@ "column.home": "خانه", "column.mutes": "کاربران بیصداشده", "column.notifications": "اعلانها", + "column.pins": "نوشتههای ثابت", "column.public": "نوشتههای همهجا", "column_back_button.label": "بازگشت", "column_header.hide_settings": "نهفتن تنظیمات", @@ -109,6 +110,7 @@ "navigation_bar.info": "اطلاعات تکمیلی", "navigation_bar.logout": "خروج", "navigation_bar.mutes": "کاربران بیصداشده", + "navigation_bar.pins": "نوشتههای ثابت", "navigation_bar.preferences": "ترجیحات", "navigation_bar.public_timeline": "نوشتههای همهجا", "notification.favourite": "{name} نوشتهٔ شما را پسندید", @@ -193,6 +195,15 @@ "upload_button.label": "افزودن تصویر", "upload_form.undo": "واگردانی", "upload_progress.label": "بارگذاری...", + "video.close": "بستن ویدیو", + "video.exit_fullscreen": "خروج از حالت تمام صفحه", + "video.expand": "بزرگکردن ویدیو", + "video.fullscreen": "تمام صفحه", + "video.hide": "نهفتن ویدیو", + "video.mute": "قطع صدا", + "video.pause": "توقف", + "video.play": "پخش", + "video.unmute": "پخش صدا", "video_player.expand": "بازکردن ویدیو", "video_player.toggle_sound": "تغییر صداداری", "video_player.toggle_visible": "تغییر پیدایی", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 926a57ff1..fc409a932 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -33,6 +33,7 @@ "column.home": "Koti", "column.mutes": "Muted users", "column.notifications": "Ilmoitukset", + "column.pins": "Pinned toot", "column.public": "Yleinen aikajana", "column_back_button.label": "Takaisin", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Extended information", "navigation_bar.logout": "Kirjaudu ulos", "navigation_bar.mutes": "Muted users", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Ominaisuudet", "navigation_bar.public_timeline": "Yleinen aikajana", "notification.favourite": "{name} tykkäsi statuksestasi", @@ -193,6 +195,15 @@ "upload_button.label": "Lisää mediaa", "upload_form.undo": "Peru", "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Expand video", "video_player.toggle_sound": "Äänet päälle/pois", "video_player.toggle_visible": "Toggle visibility", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 8ca632acc..5a436891b 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -33,8 +33,8 @@ "column.home": "Accueil", "column.mutes": "Comptes masqués", "column.notifications": "Notifications", - "column.public": "Fil public global", "column.pins": "Pouets épinglés", + "column.public": "Fil public global", "column_back_button.label": "Retour", "column_header.hide_settings": "Masquer les paramètres", "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche", @@ -110,9 +110,9 @@ "navigation_bar.info": "Plus d’informations", "navigation_bar.logout": "Déconnexion", "navigation_bar.mutes": "Comptes masqués", + "navigation_bar.pins": "Pouets épinglés", "navigation_bar.preferences": "Préférences", "navigation_bar.public_timeline": "Fil public global", - "navigation_bar.pins": "Pouets épinglés", "notification.favourite": "{name} a ajouté à ses favoris :", "notification.follow": "{name} vous suit.", "notification.mention": "{name} vous a mentionné⋅e :", @@ -166,7 +166,7 @@ "standalone.public_title": "Jeter un coup d’œil…", "status.cannot_reblog": "Cette publication ne peut être boostée", "status.delete": "Effacer", - "status.embed": "Embed", + "status.embed": "Intégrer", "status.favourite": "Ajouter aux favoris", "status.load_more": "Charger plus", "status.media_hidden": "Média caché", @@ -195,6 +195,15 @@ "upload_button.label": "Joindre un média", "upload_form.undo": "Annuler", "upload_progress.label": "Envoi en cours…", + "video.close": "Fermer la vidéo", + "video.exit_fullscreen": "Quitter plein écran", + "video.expand": "Agrandir la vidéo", + "video.fullscreen": "Plein écran", + "video.hide": "Masquer la vidéo", + "video.mute": "Couper le son", + "video.pause": "Pause", + "video.play": "Lecture", + "video.unmute": "Rétablir le son", "video_player.expand": "Agrandir la vidéo", "video_player.toggle_sound": "Activer/Désactiver le son", "video_player.toggle_visible": "Afficher/Cacher la vidéo", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 9ef933108..06b401d39 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -33,6 +33,7 @@ "column.home": "בבית", "column.mutes": "השתקות", "column.notifications": "התראות", + "column.pins": "Pinned toot", "column.public": "בפרהסיה", "column_back_button.label": "חזרה", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "מידע נוסף", "navigation_bar.logout": "יציאה", "navigation_bar.mutes": "השתקות", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "העדפות", "navigation_bar.public_timeline": "ציר זמן בין-קהילתי", "notification.favourite": "חצרוצך חובב על ידי {name}", @@ -193,6 +195,15 @@ "upload_button.label": "הוספת מדיה", "upload_form.undo": "ביטול", "upload_progress.label": "עולה...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "הרחבת וידאו", "video_player.toggle_sound": "הפעלת\\ביטול שמע", "video_player.toggle_visible": "הפעלת\\ביטול תצוגה", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index f301723cf..cb28ce9c1 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -33,6 +33,7 @@ "column.home": "Dom", "column.mutes": "Utišani korisnici", "column.notifications": "Notifikacije", + "column.pins": "Pinned toot", "column.public": "Federalni timeline", "column_back_button.label": "Natrag", "column_header.hide_settings": "Hide settings", @@ -61,7 +62,6 @@ "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.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.message": "Are you sure you want to unfollow {name}?", "embed.instructions": "Embed this status on your website by copying the code below.", @@ -110,6 +110,7 @@ "navigation_bar.info": "Više informacija", "navigation_bar.logout": "Odjavi se", "navigation_bar.mutes": "Utišani korisnici", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Postavke", "navigation_bar.public_timeline": "Federalni timeline", "notification.favourite": "{name} je lajkao tvoj status", @@ -194,6 +195,15 @@ "upload_button.label": "Dodaj media", "upload_form.undo": "Poništi", "upload_progress.label": "Uploadam...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Proširi video", "video_player.toggle_sound": "Toggle zvuk", "video_player.toggle_visible": "Preklopi vidljivost", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index a708ec638..a13e4fee2 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -33,6 +33,7 @@ "column.home": "Kezdőlap", "column.mutes": "Muted users", "column.notifications": "Értesítések", + "column.pins": "Pinned toot", "column.public": "Nyilvános", "column_back_button.label": "Vissza", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Extended information", "navigation_bar.logout": "Kijelentkezés", "navigation_bar.mutes": "Muted users", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Beállítások", "navigation_bar.public_timeline": "Nyilvános időfolyam", "notification.favourite": "{name} kedvencnek jelölte az állapotod", @@ -193,6 +195,15 @@ "upload_button.label": "Média hozzáadása", "upload_form.undo": "Mégsem", "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Expand video", "video_player.toggle_sound": "Hang kapcsolása", "video_player.toggle_visible": "Toggle visibility", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index d71e293e8..349423cce 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -33,6 +33,7 @@ "column.home": "Beranda", "column.mutes": "Pengguna dibisukan", "column.notifications": "Notifikasi", + "column.pins": "Pinned toot", "column.public": "Linimasa gabunggan", "column_back_button.label": "Kembali", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Informasi selengkapnya", "navigation_bar.logout": "Keluar", "navigation_bar.mutes": "Pengguna dibisukan", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Pengaturan", "navigation_bar.public_timeline": "Linimasa gabungan", "notification.favourite": "{name} menyukai status anda", @@ -193,6 +195,15 @@ "upload_button.label": "Tambahkan media", "upload_form.undo": "Undo", "upload_progress.label": "Mengunggah...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Tampilkan video", "video_player.toggle_sound": "Suara", "video_player.toggle_visible": "Tampilan", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 5df5c59a1..5f19509e2 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -33,6 +33,7 @@ "column.home": "Hemo", "column.mutes": "Celita uzeri", "column.notifications": "Savigi", + "column.pins": "Pinned toot", "column.public": "Federata tempolineo", "column_back_button.label": "Retro", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Detaloza informi", "navigation_bar.logout": "Ekirar", "navigation_bar.mutes": "Celita uzeri", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferi", "navigation_bar.public_timeline": "Federata tempolineo", "notification.favourite": "{name} favorizis tua mesajo", @@ -193,6 +195,15 @@ "upload_button.label": "Adjuntar kontenajo", "upload_form.undo": "Desfacar", "upload_progress.label": "Kargante...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Extensar video", "video_player.toggle_sound": "Acendar sono", "video_player.toggle_visible": "Chanjar videbleso", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index eec35a70c..cedbb947c 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -33,6 +33,7 @@ "column.home": "Home", "column.mutes": "Utenti silenziati", "column.notifications": "Notifiche", + "column.pins": "Pinned toot", "column.public": "Timeline federata", "column_back_button.label": "Indietro", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Informazioni estese", "navigation_bar.logout": "Logout", "navigation_bar.mutes": "Utenti silenziati", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Impostazioni", "navigation_bar.public_timeline": "Timeline federata", "notification.favourite": "{name} ha apprezzato il tuo post", @@ -193,6 +195,15 @@ "upload_button.label": "Aggiungi file multimediale", "upload_form.undo": "Annulla", "upload_progress.label": "Sto caricando...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Espandi video", "video_player.toggle_sound": "Attiva suono", "video_player.toggle_visible": "Attiva visibilità", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 65838a3f8..e78ac4c26 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -33,8 +33,8 @@ "column.home": "ホーム", "column.mutes": "ミュートしたユーザー", "column.notifications": "通知", - "column.public": "連合タイムライン", "column.pins": "固定されたトゥート", + "column.public": "連合タイムライン", "column_back_button.label": "戻る", "column_header.hide_settings": "設定を隠す", "column_header.moveLeft_settings": "カラムを左に移動する", @@ -97,8 +97,8 @@ "home.column_settings.show_replies": "返信表示", "home.settings": "カラム設定", "lightbox.close": "閉じる", - "lightbox.next": "Next", - "lightbox.previous": "Previous", + "lightbox.next": "次", + "lightbox.previous": "前", "loading_indicator.label": "読み込み中...", "media_gallery.toggle_visible": "表示切り替え", "missing_indicator.label": "見つかりません", @@ -110,9 +110,9 @@ "navigation_bar.info": "このインスタンスについて", "navigation_bar.logout": "ログアウト", "navigation_bar.mutes": "ミュートしたユーザー", + "navigation_bar.pins": "固定されたトゥート", "navigation_bar.preferences": "ユーザー設定", "navigation_bar.public_timeline": "連合タイムライン", - "navigation_bar.pins": "固定されたトゥート", "notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました", "notification.follow": "{name}さんにフォローされました", "notification.mention": "{name}さんがあなたに返信しました", @@ -195,6 +195,15 @@ "upload_button.label": "メディアを追加", "upload_form.undo": "やり直す", "upload_progress.label": "アップロード中...", + "video.close": "動画を閉じる", + "video.exit_fullscreen": "全画面を終了する", + "video.expand": "動画を拡大する", + "video.fullscreen": "全画面", + "video.hide": "動画を閉じる", + "video.mute": "ミュート", + "video.pause": "一時停止", + "video.play": "再生", + "video.unmute": "ミュートを解除する", "video_player.expand": "動画の詳細", "video_player.toggle_sound": "音の切り替え", "video_player.toggle_visible": "表示切り替え", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 8393e82e5..46ed772cf 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -33,8 +33,8 @@ "column.home": "홈", "column.mutes": "뮤트 중인 사용자", "column.notifications": "알림", - "column.public": "연합 타임라인", "column.pins": "고정된 Toot", + "column.public": "연합 타임라인", "column_back_button.label": "돌아가기", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", @@ -110,9 +110,9 @@ "navigation_bar.info": "이 인스턴스에 대해서", "navigation_bar.logout": "로그아웃", "navigation_bar.mutes": "뮤트 중인 사용자", + "navigation_bar.pins": "고정된 Toot", "navigation_bar.preferences": "사용자 설정", "navigation_bar.public_timeline": "연합 타임라인", - "navigation_bar.pins": "고정된 Toot", "notification.favourite": "{name}님이 즐겨찾기 했습니다", "notification.follow": "{name}님이 나를 팔로우 했습니다", "notification.mention": "{name}님이 답글을 보냈습니다", @@ -195,6 +195,15 @@ "upload_button.label": "미디어 추가", "upload_form.undo": "재시도", "upload_progress.label": "업로드 중...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "동영상 자세히 보기", "video_player.toggle_sound": "소리 토글하기", "video_player.toggle_visible": "표시 전환", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index d6775e1e4..b696bccfd 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -12,7 +12,7 @@ "account.mute": "Negeer @{name}", "account.posts": "Toots", "account.report": "Rapporteer @{name}", - "account.requested": "Wacht op goedkeuring", + "account.requested": "Wacht op goedkeuring. Klik om volgverzoek te annuleren.", "account.share": "Profiel van @{name} delen", "account.unblock": "Deblokkeer @{name}", "account.unblock_domain": "{domain} niet meer negeren", @@ -33,11 +33,13 @@ "column.home": "Start", "column.mutes": "Genegeerde gebruikers", "column.notifications": "Meldingen", + "column.pins": "Pinned toot", "column.public": "Globale tijdlijn", + "column.pins": "Vastgezette toots", "column_back_button.label": "terug", "column_header.hide_settings": "Instellingen verbergen", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", + "column_header.moveLeft_settings": "Kolom naar links verplaatsen", + "column_header.moveRight_settings": "Kolom naar rechts verplaatsen", "column_header.pin": "Vastmaken", "column_header.show_settings": "Instellingen tonen", "column_header.unpin": "Losmaken", @@ -63,8 +65,8 @@ "confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?", "confirmations.unfollow.confirm": "Ontvolgen", "confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", + "embed.instructions": "Embed deze toot op jouw website, door de onderstaande code te kopiëren.", + "embed.preview": "Zo komt het eruit te zien:", "emoji_button.activity": "Activiteiten", "emoji_button.flags": "Vlaggen", "emoji_button.food": "Eten en drinken", @@ -85,6 +87,7 @@ "follow_request.authorize": "Goedkeuren", "follow_request.reject": "Afkeuren", "getting_started.appsshort": "Apps", + "getting_started.donate": "Doneren", "getting_started.faq": "FAQ", "getting_started.heading": "Beginnen", "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.", @@ -109,8 +112,10 @@ "navigation_bar.info": "Uitgebreide informatie", "navigation_bar.logout": "Afmelden", "navigation_bar.mutes": "Genegeerde gebruikers", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Instellingen", "navigation_bar.public_timeline": "Globale tijdlijn", + "navigation_bar.pins": "Vastgezette toots", "notification.favourite": "{name} markeerde jouw toot als favoriet", "notification.follow": "{name} volgt jou nu", "notification.mention": "{name} vermeldde jou", @@ -171,7 +176,7 @@ "status.mention": "Vermeld @{name}", "status.mute_conversation": "Negeer conversatie", "status.open": "Toot volledig tonen", - "status.pin": "Pin on profile", + "status.pin": "Aan profielpagina vastmaken", "status.reblog": "Boost", "status.reblogged_by": "{name} boostte", "status.reply": "Reageren", @@ -183,7 +188,7 @@ "status.show_less": "Minder tonen", "status.show_more": "Meer tonen", "status.unmute_conversation": "Conversatie niet meer negeren", - "status.unpin": "Unpin from profile", + "status.unpin": "Van profielpagina losmaken", "tabs_bar.compose": "Schrijven", "tabs_bar.federated_timeline": "Globaal", "tabs_bar.home": "Start", @@ -193,6 +198,15 @@ "upload_button.label": "Media toevoegen", "upload_form.undo": "Ongedaan maken", "upload_progress.label": "Uploaden...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Video groter maken", + "video.fullscreen": "Volledig scherm", + "video.hide": "Video verbergen", + "video.mute": "Geluid uitschakelen", + "video.pause": "Pauze", + "video.play": "Afspelen", + "video.unmute": "Geluid inschakelen", "video_player.expand": "Video groter maken", "video_player.toggle_sound": "Geluid in-/uitschakelen", "video_player.toggle_visible": "Video wel/niet tonen", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index f3c24a807..742017c66 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -33,6 +33,7 @@ "column.home": "Hjem", "column.mutes": "Dempede brukere", "column.notifications": "Varsler", + "column.pins": "Pinned toot", "column.public": "Felles tidslinje", "column_back_button.label": "Tilbake", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Utvidet informasjon", "navigation_bar.logout": "Logg ut", "navigation_bar.mutes": "Dempede brukere", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferanser", "navigation_bar.public_timeline": "Felles tidslinje", "notification.favourite": "{name} likte din status", @@ -193,6 +195,15 @@ "upload_button.label": "Legg til media", "upload_form.undo": "Angre", "upload_progress.label": "Laster opp...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Utvid video", "video_player.toggle_sound": "Veksle lyd", "video_player.toggle_visible": "Veksle synlighet", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index d2b2dd48f..512e4120d 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -33,8 +33,8 @@ "column.home": "Acuèlh", "column.mutes": "Personas en silenci", "column.notifications": "Notificacions", - "column.public": "Flux public global", "column.pins": "Tuts penjats", + "column.public": "Flux public global", "column_back_button.label": "Tornar", "column_header.hide_settings": "Amagar los paramètres", "column_header.moveLeft_settings": "Desplaçar la colomna a man drecha", @@ -64,7 +64,7 @@ "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?", "confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?", - "embed.instructions": "Embarcar aqueste estatut per o far veire sus un site Internet en copiar lo còdi çai-jos.", + "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.", "embed.preview": "Semblarà aquò : ", "emoji_button.activity": "Activitats", "emoji_button.flags": "Drapèus", @@ -110,9 +110,9 @@ "navigation_bar.info": "Mai informacions", "navigation_bar.logout": "Desconnexion", "navigation_bar.mutes": "Personas rescondudas", + "navigation_bar.pins": "Tuts penjats", "navigation_bar.preferences": "Preferéncias", "navigation_bar.public_timeline": "Flux public global", - "navigation_bar.pins": "Tuts penjats", "notification.favourite": "{name} a ajustat a sos favorits :", "notification.follow": "{name} vos sèc", "notification.mention": "{name} vos a mencionat :", @@ -195,6 +195,15 @@ "upload_button.label": "Ajustar un mèdia", "upload_form.undo": "Anullar", "upload_progress.label": "Mandadís…", + "video.close": "Tampar la vidèo", + "video.exit_fullscreen": "Sortir plen ecran", + "video.expand": "Agrandir la vidèo", + "video.fullscreen": "Ecran complet", + "video.hide": "Amagar la vidèo", + "video.mute": "Copar lo son", + "video.pause": "Pausa", + "video.play": "Lectura", + "video.unmute": "Restablir lo son", "video_player.expand": "Mostrar la vidèo", "video_player.toggle_sound": "Activar/Desactivar lo son", "video_player.toggle_visible": "Mostrar/Rescondre la vidèo", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index daa60128d..1d2443690 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -195,7 +195,16 @@ "upload_button.label": "Dodaj zawartość multimedialną", "upload_form.undo": "Cofnij", "upload_progress.label": "Wysyłanie", - "video_player.expand": "Przełącz wideo", + "video.close": "Zamknij film", + "video.exit_fullscreen": "Opuść tryb pełnoekranowy", + "video.expand": "Rozszerz film", + "video.fullscreen": "Pełny ekran", + "video.hide": "Ukryj film", + "video.mute": "Wycisz", + "video.pause": "Pauzuj", + "video.play": "Odtwórz", + "video.unmute": "Cofnij wyciszenie", + "video_player.expand": "Rozszerz film", "video_player.toggle_sound": "Przełącz dźwięk", "video_player.toggle_visible": "Przełącz widoczność", "video_player.video_error": "Nie można odtworzyć pliku wideo" diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index e861bf73f..a5def0ad0 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -6,25 +6,25 @@ "account.follow": "Seguir", "account.followers": "Seguidores", "account.follows": "Segue", - "account.follows_you": "É seu seguidor", + "account.follows_you": "Segue você", "account.media": "Mídia", "account.mention": "Mencionar @{name}", "account.mute": "Silenciar @{name}", "account.posts": "Posts", "account.report": "Denunciar @{name}", - "account.requested": "Aguardando aprovação", + "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação.", "account.share": "Compartilhar perfil de @{name}", - "account.unblock": "Não bloquear @{name}", + "account.unblock": "Desbloquear @{name}", "account.unblock_domain": "Desbloquear {domain}", "account.unfollow": "Deixar de seguir", "account.unmute": "Não silenciar @{name}", "account.view_full_profile": "Ver perfil completo", - "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", - "bundle_column_error.body": "Something went wrong while loading this component.", + "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez", + "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.", "bundle_column_error.retry": "Tente novamente", - "bundle_column_error.title": "Network error", + "bundle_column_error.title": "Erro de rede", "bundle_modal_error.close": "Fechar", - "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.", "bundle_modal_error.retry": "Tente novamente", "column.blocks": "Usuários bloqueados", "column.community": "Local", @@ -33,7 +33,9 @@ "column.home": "Página inicial", "column.mutes": "Usuários silenciados", "column.notifications": "Notificações", + "column.pins": "Postagens fixadas", "column.public": "Global", + "column.pins": "Postagens fixadas", "column_back_button.label": "Voltar", "column_header.hide_settings": "Esconder configurações", "column_header.moveLeft_settings": "Mover coluna para a esquerda", @@ -43,156 +45,169 @@ "column_header.unpin": "Desafixar", "column_subheading.navigation": "Navegação", "column_subheading.settings": "Configurações", - "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar as suas postagens só para seguidores.", - "compose_form.lock_disclaimer.lock": "locked", + "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.", + "compose_form.lock_disclaimer.lock": "trancado", "compose_form.placeholder": "No que você está pensando?", - "compose_form.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários do {domains}. Você confia {domainsCount, plural, one {neste servidor} other {nestes servidores}}? As configurações de privacidade só funcionam em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não há como garantir a privacidade de suas postagens, e elas podem ser compartilhadas com outros.", + "compose_form.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários de {domains}. Você confia {domainsCount, plural, one {neste servidor} other {nestes servidores}}? As configurações de privacidade só funcionam em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não há como garantir a privacidade de suas postagens, e elas podem ser compartilhadas com destinatários indesejados.", "compose_form.publish": "Publicar", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "Marcar mídia como conteúdo sensível", - "compose_form.spoiler": "Esconder texto com aviso", + "compose_form.spoiler": "Esconder texto com aviso de conteúdo", "compose_form.spoiler_placeholder": "Aviso de conteúdo", "confirmation_modal.cancel": "Cancelar", "confirmations.block.confirm": "Bloquear", "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?", "confirmations.delete.confirm": "Excluir", - "confirmations.delete.message": "Você tem certeza de que quer excluir este status?", + "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?", "confirmations.domain_block.confirm": "Esconder o domínio inteiro", "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.", "confirmations.mute.confirm": "Silenciar", "confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?", "confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", - "emoji_button.activity": "Activity", - "emoji_button.flags": "Flags", - "emoji_button.food": "Food & Drink", + "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:", + "embed.preview": "Aqui está uma previsão de como ficará:", + "emoji_button.activity": "Atividades", + "emoji_button.flags": "Bandeiras", + "emoji_button.food": "Comidas & Bebidas", "emoji_button.label": "Inserir Emoji", - "emoji_button.nature": "Nature", - "emoji_button.objects": "Objects", - "emoji_button.people": "People", - "emoji_button.search": "Search...", - "emoji_button.symbols": "Symbols", - "emoji_button.travel": "Travel & Places", - "empty_column.community": "Ainda não existem conteúdo local para mostrar!", - "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag", - "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", - "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.", + "emoji_button.nature": "Natureza", + "emoji_button.objects": "Objetos", + "emoji_button.people": "Pessoas", + "emoji_button.search": "Buscar...", + "emoji_button.symbols": "Símbolos", + "emoji_button.travel": "Viagens & Lugares", + "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!", + "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag", + "empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.", + "empty_column.home.inactivity": "A sua página inicial está vazia. Se você esteve inativo por um tempo, ela irá se regenerar em alguns intantes.", "empty_column.home.public_timeline": "global", - "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", - "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", + "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!", + "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.", "follow_request.authorize": "Autorizar", "follow_request.reject": "Rejeitar", "getting_started.appsshort": "Apps", "getting_started.faq": "FAQ", "getting_started.heading": "Primeiros passos", - "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.", - "getting_started.userguide": "User Guide", + "getting_started.open_source_notice": "Mastodon é um software de código aberto. Você pode contribuir ou reportar problemas na página do GitHub do projeto: {github}.", + "getting_started.userguide": "Guia de usuário", "home.column_settings.advanced": "Avançado", "home.column_settings.basic": "Básico", "home.column_settings.filter_regex": "Filtrar com uma expressão regular", - "home.column_settings.show_reblogs": "Mostrar as partilhas", + "home.column_settings.show_reblogs": "Mostrar compartilhamentos", "home.column_settings.show_replies": "Mostrar as respostas", - "home.settings": "Parâmetros da listagem", + "home.settings": "Configurações de colunas", "lightbox.close": "Fechar", - "lightbox.next": "Next", - "lightbox.previous": "Previous", + "lightbox.next": "Próximo", + "lightbox.previous": "Anterior", "loading_indicator.label": "Carregando...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", - "navigation_bar.blocks": "Utilizadores bloqueados", + "navigation_bar.blocks": "Usuários bloqueados", "navigation_bar.community_timeline": "Local", "navigation_bar.edit_profile": "Editar perfil", "navigation_bar.favourites": "Favoritos", "navigation_bar.follow_requests": "Seguidores pendentes", "navigation_bar.info": "Mais informações", "navigation_bar.logout": "Sair", - "navigation_bar.mutes": "Utilizadores silenciados", + "navigation_bar.mutes": "Usuários silenciados", + "navigation_bar.pins": "Postagens fixadas", "navigation_bar.preferences": "Preferências", "navigation_bar.public_timeline": "Global", - "notification.favourite": "{name} adicionou o teu post aos favoritos", - "notification.follow": "{name} seguiu-te", - "notification.mention": "{name} mencionou-te", - "notification.reblog": "{name} partilhou o teu post", + "navigation_bar.preferences": "Preferências", + "navigation_bar.public_timeline": "Global", + "navigation_bar.pins": "Postagens fixadas", + "notification.favourite": "{name} adicionou a sua postagem aos favoritos", + "notification.follow": "{name} te seguiu", + "notification.mention": "{name} te mencionou", + "notification.reblog": "{name} compartilhou a sua postagem", "notifications.clear": "Limpar notificações", - "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", + "notifications.clear_confirmation": "Você tem certeza de que quer limpar todas as suas notificações permanentemente?", "notifications.column_settings.alert": "Notificações no computador", "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.mention": "Menções:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.push_meta": "This device", - "notifications.column_settings.reblog": "Partilhas:", + "notifications.column_settings.push": "Enviar notificações", + "notifications.column_settings.push_meta": "Este aparelho", + "notifications.column_settings.reblog": "Compartilhamento:", "notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.sound": "Reproduzir som", - "onboarding.done": "Done", - "onboarding.next": "Next", - "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", - "onboarding.page_four.home": "The home timeline shows posts from people you follow.", - "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", - "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", - "onboarding.page_one.welcome": "Welcome to Mastodon!", - "onboarding.page_six.admin": "Your instance's admin is {admin}.", - "onboarding.page_six.almost_done": "Almost done...", + "onboarding.done": "Pronto", + "onboarding.next": "Próximo", + "onboarding.page_five.public_timelines": "A timeline local mostra postagens públicas de todos os usuários no {domain}. A timeline federada mostra todas as postagens de todas as pessoas que pessoas no {domain} seguem. Estas são as timelines públicas, uma ótima maneira de conhecer novas pessoas.", + "onboarding.page_four.home": "A página inicial mostra postagens de pessoas que você segue.", + "onboarding.page_four.notifications": "A coluna de notificações te mostra quando alguém interage com você.", + "onboarding.page_one.federation": "Mastodon é uma rede d servidores independentes se juntando para fazer uma grande rede social. Nós chamamos estes servidores de instâncias.", + "onboarding.page_one.handle": "Você está no {domain}, então o seu nome de usuário completo é {handle}", + "onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!", + "onboarding.page_six.admin": "O administrador de sua instância é {admin}.", + "onboarding.page_six.almost_done": "Quase acabando...", "onboarding.page_six.appetoot": "Bon Appetoot!", - "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", - "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", - "onboarding.page_six.guidelines": "community guidelines", - "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", - "onboarding.page_six.various_app": "mobile apps", - "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", - "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", - "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", - "onboarding.skip": "Skip", + "onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.", + "onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Você pode reportar bugs, prequisitar novas funções ou contribuir para o código no {github}.", + "onboarding.page_six.guidelines": "diretrizes da comunidade", + "onboarding.page_six.read_guidelines": "Por favor, leia as {guidelines} do {domain}!", + "onboarding.page_six.various_app": "aplicativos móveis", + "onboarding.page_three.profile": "Edite o seu perfil para mudar o seu o seu avatar, bio e nome de exibição. No menu de configurações, você também encontrará outras preferências.", + "onboarding.page_three.search": "Use a barra de buscas para encontrar pessoas e consultar hashtahs, como #illustrations e #introductions. Para procurar por uma pessoa que não estiver nesta instância, use o nome de usuário completo dela.", + "onboarding.page_two.compose": "Escreva postagens na coluna de escrita. Você pode hospedar imagens, mudar as configurações de privacidade e adicionar alertas de conteúdo através dos ícones abaixo.", + "onboarding.skip": "Pular", "privacy.change": "Ajustar a privacidade da mensagem", - "privacy.direct.long": "Apenas para utilizadores mencionados", - "privacy.direct.short": "Directo", - "privacy.private.long": "Apenas para os seguidores", - "privacy.private.short": "Privado", + "privacy.direct.long": "Apenas para usuários mencionados", + "privacy.direct.short": "Direta", + "privacy.private.long": "Apenas para seus seguidores", + "privacy.private.short": "Privada", "privacy.public.long": "Publicar em todos os feeds", - "privacy.public.short": "Público", - "privacy.unlisted.long": "Não publicar nos feeds públicos", - "privacy.unlisted.short": "Não listar", + "privacy.public.short": "Pública", + "privacy.unlisted.long": "Não publicar em feeds públicos", + "privacy.unlisted.short": "Não listada", "reply_indicator.cancel": "Cancelar", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", - "standalone.public_title": "A look inside...", - "status.cannot_reblog": "This post cannot be boosted", + "standalone.public_title": "Dê uma espiada...", + "status.cannot_reblog": "Esta postagem não pode ser compartilhada", "status.delete": "Eliminar", - "status.embed": "Embed", + "status.embed": "Incorporar", "status.favourite": "Adicionar aos favoritos", "status.load_more": "Carregar mais", - "status.media_hidden": "Media escondida", + "status.media_hidden": "Mídia escondida", "status.mention": "Mencionar @{name}", - "status.mute_conversation": "Mute conversation", + "status.mute_conversation": "Silenciar conversa", "status.open": "Expandir", - "status.pin": "Pin on profile", - "status.reblog": "Partilhar", - "status.reblogged_by": "{name} partilhou", + "status.pin": "Fixar no perfil", + "status.reblog": "Compartilhar", + "status.reblogged_by": "{name} compartilhou", "status.reply": "Responder", - "status.replyAll": "Reply to thread", - "status.report": "Denúnciar @{name}", + "status.replyAll": "Responder à sequência", + "status.report": "Denunciar @{name}", "status.sensitive_toggle": "Clique para ver", "status.sensitive_warning": "Conteúdo sensível", - "status.share": "Share", + "status.share": "Compartilhar", "status.show_less": "Mostrar menos", "status.show_more": "Mostrar mais", - "status.unmute_conversation": "Unmute conversation", - "status.unpin": "Unpin from profile", + "status.unmute_conversation": "Desativar silêncio desta conversa", + "status.unpin": "Desafixar do perfil", "tabs_bar.compose": "Criar", "tabs_bar.federated_timeline": "Global", - "tabs_bar.home": "Home", + "tabs_bar.home": "Página inicial", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificações", "upload_area.title": "Arraste e solte para enviar", - "upload_button.label": "Adicionar media", + "upload_button.label": "Adicionar mídia", "upload_form.undo": "Anular", - "upload_progress.label": "A gravar...", + "upload_progress.label": "Salvando...", + "video.close": "Fechar vídeo", + "video.exit_fullscreen": "Sair da tela cheia", + "video.expand": "Expandir vídeo", + "video.fullscreen": "Tela cheia", + "video.hide": "Esconder vídeo", + "video.mute": "Silenciar vídeo", + "video.pause": "Parar", + "video.play": "Reproduzir", + "video.unmute": "Retirar silêncio", "video_player.expand": "Expandir vídeo", "video_player.toggle_sound": "Ligar/Desligar som", "video_player.toggle_visible": "Ligar/Desligar vídeo", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index f9e686411..cff528f83 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -33,6 +33,7 @@ "column.home": "Home", "column.mutes": "Utilizadores silenciados", "column.notifications": "Notificações", + "column.pins": "Pinned toot", "column.public": "Global", "column_back_button.label": "Voltar", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Mais informações", "navigation_bar.logout": "Sair", "navigation_bar.mutes": "Utilizadores silenciados", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferências", "navigation_bar.public_timeline": "Global", "notification.favourite": "{name} adicionou o teu post aos favoritos", @@ -193,6 +195,15 @@ "upload_button.label": "Adicionar media", "upload_form.undo": "Anular", "upload_progress.label": "A gravar...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Expandir vídeo", "video_player.toggle_sound": "Ligar/Desligar som", "video_player.toggle_visible": "Ligar/Desligar vídeo", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 0f78f4b17..fcc147c87 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -33,6 +33,7 @@ "column.home": "Главная", "column.mutes": "Список глушения", "column.notifications": "Уведомления", + "column.pins": "Pinned toot", "column.public": "Глобальная лента", "column_back_button.label": "Назад", "column_header.hide_settings": "Скрыть настройки", @@ -109,6 +110,7 @@ "navigation_bar.info": "Об узле", "navigation_bar.logout": "Выйти", "navigation_bar.mutes": "Список глушения", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Опции", "navigation_bar.public_timeline": "Глобальная лента", "notification.favourite": "{name} понравился Ваш статус", @@ -193,6 +195,15 @@ "upload_button.label": "Добавить медиаконтент", "upload_form.undo": "Отменить", "upload_progress.label": "Загрузка...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Развернуть видео", "video_player.toggle_sound": "Вкл./выкл. звук", "video_player.toggle_visible": "Показать/скрыть", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 069fdf7c3..f2752f5e0 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -33,6 +33,7 @@ "column.home": "Home", "column.mutes": "Muted users", "column.notifications": "Notifications", + "column.pins": "Pinned toot", "column.public": "Federated timeline", "column_back_button.label": "Back", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "About this instance", "navigation_bar.logout": "Logout", "navigation_bar.mutes": "Muted users", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferences", "navigation_bar.public_timeline": "Federated timeline", "notification.favourite": "{name} favourited your status", @@ -193,6 +195,15 @@ "upload_button.label": "Add media", "upload_form.undo": "Undo", "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Expand video", "video_player.toggle_sound": "Toggle sound", "video_player.toggle_visible": "Toggle visibility", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 8a36bd207..2676b851c 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -33,6 +33,7 @@ "column.home": "Anasayfa", "column.mutes": "Susturulmuş kullanıcılar", "column.notifications": "Bildirimler", + "column.pins": "Pinned toot", "column.public": "Federe zaman tüneli", "column_back_button.label": "Geri", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Genişletilmiş bilgi", "navigation_bar.logout": "Çıkış", "navigation_bar.mutes": "Sessize alınmış kullanıcılar", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Tercihler", "navigation_bar.public_timeline": "Federe zaman tüneli", "notification.favourite": "{name} senin durumunu favorilere ekledi", @@ -193,6 +195,15 @@ "upload_button.label": "Görsel ekle", "upload_form.undo": "Geri al", "upload_progress.label": "Yükleniyor...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Videoyu genişlet", "video_player.toggle_sound": "Sesi aç/kapa", "video_player.toggle_visible": "Göster/gizle", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 1d06218e6..6b5ab64ef 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -33,6 +33,7 @@ "column.home": "Головна", "column.mutes": "Заглушені користувачі", "column.notifications": "Сповіщення", + "column.pins": "Pinned toot", "column.public": "Глобальна стрічка", "column_back_button.label": "Назад", "column_header.hide_settings": "Hide settings", @@ -109,6 +110,7 @@ "navigation_bar.info": "Про інстанцію", "navigation_bar.logout": "Вийти", "navigation_bar.mutes": "Заглушені користувачі", + "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Налаштування", "navigation_bar.public_timeline": "Глобальна стрічка", "notification.favourite": "{name} сподобався ваш допис", @@ -193,6 +195,15 @@ "upload_button.label": "Додати медіаконтент", "upload_form.undo": "Відмінити", "upload_progress.label": "Завантаження...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", "video_player.expand": "Розгорнути ", "video_player.toggle_sound": "Увімкнути/вимкнути звук", "video_player.toggle_visible": "Показати/приховати", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 93faf8876..6037e7581 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -1,13 +1,13 @@ { "account.block": "屏蔽 @{name}", - "account.block_domain": "Hide everything from {domain}", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.block_domain": "隐藏一切来自 {domain} 的嘟文", + "account.disclaimer_full": "下列资料不一定完整。", "account.edit_profile": "修改个人资料", "account.follow": "关注", "account.followers": "关注者", - "account.follows": "正在关注", + "account.follows": "正关注", "account.follows_you": "关注你", - "account.media": "Media", + "account.media": "媒体", "account.mention": "提及 @{name}", "account.mute": "将 @{name} 静音", "account.posts": "嘟文", @@ -15,40 +15,41 @@ "account.requested": "等待审批", "account.share": "分享 @{name}的个人资料", "account.unblock": "解除对 @{name} 的屏蔽", - "account.unblock_domain": "解除封锁 {domain}", + "account.unblock_domain": "不再隐藏 {domain}", "account.unfollow": "取消关注", "account.unmute": "取消 @{name} 的静音", "account.view_full_profile": "查看完整资料", "boost_modal.combo": "如你想在下次路过时显示,请按{combo},", "bundle_column_error.body": "载入组件出错。", - "bundle_column_error.retry": "再次尝试", + "bundle_column_error.retry": "重试", "bundle_column_error.title": "网络错误", "bundle_modal_error.close": "关闭", "bundle_modal_error.message": "载入组件出错。", - "bundle_modal_error.retry": "再次尝试", + "bundle_modal_error.retry": "重试", "column.blocks": "屏蔽用户", "column.community": "本站时间轴", - "column.favourites": "赞过的嘟文", + "column.favourites": "收藏过的嘟文", "column.follow_requests": "关注请求", "column.home": "主页", "column.mutes": "被静音的用户", "column.notifications": "通知", + "column.pins": "Pinned toot", "column.public": "跨站公共时间轴", "column_back_button.label": "返回", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", - "column_header.pin": "Pin", - "column_header.show_settings": "Show settings", - "column_header.unpin": "Unpin", + "column_header.hide_settings": "隐藏设置", + "column_header.moveLeft_settings": "将栏左移", + "column_header.moveRight_settings": "将栏右移", + "column_header.pin": "置顶", + "column_header.show_settings": "显示设置", + "column_header.unpin": "撤顶", "column_subheading.navigation": "导航", "column_subheading.settings": "设置", - "compose_form.lock_disclaimer": "你的账户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.", + "compose_form.lock_disclaimer": "你的帐户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.", "compose_form.lock_disclaimer.lock": "被保护", "compose_form.placeholder": "在想啥?", "compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务器实例,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务器实例} other {之中有些不是 Mastodon 服务器实例}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。", "compose_form.publish": "嘟嘟", - "compose_form.publish_loud": "{publish}!", + "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "将媒体文件标示为“敏感内容”", "compose_form.spoiler": "将部分文本藏于警告消息之后", "compose_form.spoiler_placeholder": "敏感内容的警告消息", @@ -57,14 +58,14 @@ "confirmations.block.message": "想好了,真的要屏蔽 {name}?", "confirmations.delete.confirm": "删除", "confirmations.delete.message": "想好了,真的要删除这条嘟文?", - "confirmations.domain_block.confirm": "Hide entire domain", - "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", + "confirmations.domain_block.confirm": "隐藏整个网站", + "confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain} ?多数情况下,封锁或静音几个特定目标就好。", "confirmations.mute.confirm": "静音", "confirmations.mute.message": "想好了,真的要静音 {name}?", "confirmations.unfollow.confirm": "取消关注", "confirmations.unfollow.message": "确定要取消关注 {name}吗?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", + "embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。", + "embed.preview": "到时大概长这样:", "emoji_button.activity": "活动", "emoji_button.flags": "旗帜", "emoji_button.food": "食物和饮料", @@ -72,13 +73,13 @@ "emoji_button.nature": "自然", "emoji_button.objects": "物体", "emoji_button.people": "人物", - "emoji_button.search": "搜索...", + "emoji_button.search": "搜索…", "emoji_button.symbols": "符号", "emoji_button.travel": "旅途和地点", - "empty_column.community": "本站时间轴暂时未有内容,快贴文来抢头香啊!", + "empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!", "empty_column.hashtag": "这个标签暂时未有内容。", "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", - "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.", + "empty_column.home.inactivity": "你的主页暂时没有内容。也许你太久没有来了?如果是这样,文章会慢慢出来,请稍后再看。", "empty_column.home.public_timeline": "公共时间轴", "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。", "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。", @@ -96,33 +97,34 @@ "home.column_settings.show_replies": "显示回应嘟文", "home.settings": "字段设置", "lightbox.close": "关闭", - "lightbox.next": "Next", - "lightbox.previous": "Previous", + "lightbox.next": "下一步", + "lightbox.previous": "上一步", "loading_indicator.label": "加载中……", "media_gallery.toggle_visible": "打开或关上", "missing_indicator.label": "找不到内容", "navigation_bar.blocks": "被屏蔽的用户", "navigation_bar.community_timeline": "本站时间轴", "navigation_bar.edit_profile": "修改个人资料", - "navigation_bar.favourites": "赞的内容", + "navigation_bar.favourites": "收藏的内容", "navigation_bar.follow_requests": "关注请求", "navigation_bar.info": "关于本站", "navigation_bar.logout": "注销", "navigation_bar.mutes": "被静音的用户", + "navigation_bar.pins": "置顶嘟文", "navigation_bar.preferences": "首选项", "navigation_bar.public_timeline": "跨站公共时间轴", - "notification.favourite": "{name} 赞了你的嘟文", + "notification.favourite": "{name} 收藏了你的嘟文", "notification.follow": "{name} 开始关注你", "notification.mention": "{name} 提及你", "notification.reblog": "{name} 转嘟了你的嘟文", "notifications.clear": "清空通知纪录", "notifications.clear_confirmation": "你确定要清空通知纪录吗?", "notifications.column_settings.alert": "显示桌面通知", - "notifications.column_settings.favourite": "你的嘟文被赞:", + "notifications.column_settings.favourite": "你的嘟文被收藏:", "notifications.column_settings.follow": "关注你:", "notifications.column_settings.mention": "提及你:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.push_meta": "This device", + "notifications.column_settings.push": "推送通知", + "notifications.column_settings.push_meta": "此设备", "notifications.column_settings.reblog": "你的嘟文被转嘟:", "notifications.column_settings.show": "在通知栏显示", "notifications.column_settings.sound": "播放音效", @@ -132,18 +134,18 @@ "onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.", "onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~", "onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络,我们将这些独立但又相互连接的服务器叫做服务器实例。", - "onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整账户名称。", + "onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整帐户名称。", "onboarding.page_one.welcome": "欢迎来到 Mastodon!", "onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.", - "onboarding.page_six.almost_done": "快完成了...", + "onboarding.page_six.almost_done": "差不多了…", "onboarding.page_six.appetoot": "嗷呜~", "onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~", "onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎来 {github} 报告问题,提交功能请求,或者贡献代码 :-)", "onboarding.page_six.guidelines": "社区指南", - "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!", + "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!", "onboarding.page_six.various_app": "移动应用程序", "onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。", - "onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整账户名称(用户名@域名)啦。", + "onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整帐户名称(用户名@域名)啦。", "onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.", "onboarding.skip": "好啦好啦我知道啦", "privacy.change": "调整隐私设置", @@ -161,29 +163,29 @@ "report.target": "Reporting", "search.placeholder": "搜索", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "standalone.public_title": "A look inside...", + "standalone.public_title": "大家都在干啥?", "status.cannot_reblog": "没法转嘟这条嘟文啦……", "status.delete": "删除", - "status.embed": "Embed", - "status.favourite": "赞", + "status.embed": "嵌入", + "status.favourite": "收藏", "status.load_more": "加载更多", "status.media_hidden": "隐藏媒体内容", "status.mention": "提及 @{name}", - "status.mute_conversation": "Mute conversation", + "status.mute_conversation": "静音对话", "status.open": "展开嘟文", - "status.pin": "Pin on profile", + "status.pin": "置顶到资料", "status.reblog": "转嘟", "status.reblogged_by": "{name} 转嘟", "status.reply": "回应", - "status.replyAll": "Reply to thread", + "status.replyAll": "回应整串", "status.report": "举报 @{name}", "status.sensitive_toggle": "点击显示", "status.sensitive_warning": "敏感内容", "status.share": "Share", "status.show_less": "减少显示", "status.show_more": "显示更多", - "status.unmute_conversation": "Unmute conversation", - "status.unpin": "Unpin from profile", + "status.unmute_conversation": "解禁对话", + "status.unpin": "解除置顶", "tabs_bar.compose": "撰写", "tabs_bar.federated_timeline": "跨站", "tabs_bar.home": "主页", @@ -193,6 +195,15 @@ "upload_button.label": "上传媒体文件", "upload_form.undo": "还原", "upload_progress.label": "上传中……", + "video.close": "关闭影片", + "video.exit_fullscreen": "退出全荧幕", + "video.expand": "展开影片", + "video.fullscreen": "全荧幕", + "video.hide": "隐藏影片", + "video.mute": "静音", + "video.pause": "暂停", + "video.play": "播放", + "video.unmute": "解除静音", "video_player.expand": "展开影片", "video_player.toggle_sound": "开关音效", "video_player.toggle_visible": "打开或关上", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index d689cd5ae..66d32fb7e 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -1,46 +1,47 @@ { "account.block": "封鎖 @{name}", - "account.block_domain": "Hide everything from {domain}", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.block_domain": "隱藏來自 {domain} 的一切文章", + "account.disclaimer_full": "下列資料不一定完整。", "account.edit_profile": "修改個人資料", "account.follow": "關注", "account.followers": "關注的人", - "account.follows": "正在關注", + "account.follows": "正關注", "account.follows_you": "關注你", - "account.media": "Media", + "account.media": "媒體", "account.mention": "提及 @{name}", "account.mute": "將 @{name} 靜音", "account.posts": "文章", "account.report": "舉報 @{name}", "account.requested": "等候審批", - "account.share": "Share @{name}'s profile", + "account.share": "分享 @{name} 的個人資料", "account.unblock": "解除對 @{name} 的封鎖", - "account.unblock_domain": "Unhide {domain}", + "account.unblock_domain": "不再隱藏 {domain}", "account.unfollow": "取消關注", "account.unmute": "取消 @{name} 的靜音", - "account.view_full_profile": "View full profile", + "account.view_full_profile": "查看完整資料", "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},", - "bundle_column_error.body": "Something went wrong while loading this component.", - "bundle_column_error.retry": "Try again", - "bundle_column_error.title": "Network error", - "bundle_modal_error.close": "Close", - "bundle_modal_error.message": "Something went wrong while loading this component.", - "bundle_modal_error.retry": "Try again", + "bundle_column_error.body": "加載本組件出錯。", + "bundle_column_error.retry": "重試", + "bundle_column_error.title": "網絡錯誤", + "bundle_modal_error.close": "關閉", + "bundle_modal_error.message": "加載本組件出錯。", + "bundle_modal_error.retry": "重試", "column.blocks": "封鎖用戶", "column.community": "本站時間軸", - "column.favourites": "喜歡的文章", + "column.favourites": "最愛的文章", "column.follow_requests": "關注請求", "column.home": "主頁", "column.mutes": "靜音名單", "column.notifications": "通知", + "column.pins": "Pinned toot", "column.public": "跨站時間軸", "column_back_button.label": "返回", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", - "column_header.pin": "Pin", - "column_header.show_settings": "Show settings", - "column_header.unpin": "Unpin", + "column_header.hide_settings": "隱藏設定", + "column_header.moveLeft_settings": "將欄左移", + "column_header.moveRight_settings": "將欄右移", + "column_header.pin": "置頂", + "column_header.show_settings": "顯示設定", + "column_header.unpin": "撤頂", "column_subheading.navigation": "瀏覽", "column_subheading.settings": "設定", "compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。", @@ -48,7 +49,7 @@ "compose_form.placeholder": "你在想甚麼?", "compose_form.privacy_disclaimer": "你的私人文章,將被遞送至 {domains}。你是否信任{domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將可無視文章的私隱設定,轉推文章給其他用戶閱讀。", "compose_form.publish": "發文", - "compose_form.publish_loud": "{publish}!", + "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "將媒體檔案標示為「敏感內容」", "compose_form.spoiler": "將部份文字藏於警告訊息之後", "compose_form.spoiler_placeholder": "敏感警告訊息", @@ -57,14 +58,14 @@ "confirmations.block.message": "你確定要封鎖{name}嗎?", "confirmations.delete.confirm": "刪除", "confirmations.delete.message": "你確定要刪除{name}嗎?", - "confirmations.domain_block.confirm": "Hide entire domain", - "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", + "confirmations.domain_block.confirm": "隱藏整個網站", + "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。", "confirmations.mute.confirm": "靜音", "confirmations.mute.message": "你確定要將{name}靜音嗎?", - "confirmations.unfollow.confirm": "Unfollow", - "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.preview": "Here is what it will look like:", + "confirmations.unfollow.confirm": "取消關注", + "confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?", + "embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。", + "embed.preview": "看上去會是這樣:", "emoji_button.activity": "活動", "emoji_button.flags": "旗幟", "emoji_button.food": "飲飲食食", @@ -75,7 +76,7 @@ "emoji_button.search": "搜尋…", "emoji_button.symbols": "符號", "emoji_button.travel": "旅遊景物", - "empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!", + "empty_column.community": "本站時間軸暫時未有內容,快文章來搶頭香啊!", "empty_column.hashtag": "這個標籤暫時未有內容。", "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。", "empty_column.home.inactivity": "你的主頁暫時沒有內容。也許你太久沒有來?如果是這樣,文章會慢慢出來,請稍後再看。", @@ -96,34 +97,35 @@ "home.column_settings.show_replies": "顯示回應文章", "home.settings": "欄位設定", "lightbox.close": "關閉", - "lightbox.next": "Next", - "lightbox.previous": "Previous", + "lightbox.next": "繼續", + "lightbox.previous": "回退", "loading_indicator.label": "載入中...", "media_gallery.toggle_visible": "打開或關上", "missing_indicator.label": "找不到內容", "navigation_bar.blocks": "被你封鎖的用戶", "navigation_bar.community_timeline": "本站時間軸", "navigation_bar.edit_profile": "修改個人資料", - "navigation_bar.favourites": "喜歡的內容", + "navigation_bar.favourites": "最愛的內容", "navigation_bar.follow_requests": "關注請求", "navigation_bar.info": "關於本服務站", "navigation_bar.logout": "登出", "navigation_bar.mutes": "被你靜音的用戶", + "navigation_bar.pins": "置頂文章", "navigation_bar.preferences": "偏好設定", "navigation_bar.public_timeline": "跨站時間軸", - "notification.favourite": "{name} 喜歡你的文章", + "notification.favourite": "{name} 收藏了你的文章", "notification.follow": "{name} 開始關注你", "notification.mention": "{name} 提及你", "notification.reblog": "{name} 轉推你的文章", "notifications.clear": "清空通知紀錄", "notifications.clear_confirmation": "你確定要清空通知紀錄嗎?", "notifications.column_settings.alert": "顯示桌面通知", - "notifications.column_settings.favourite": "喜歡你的文章:", - "notifications.column_settings.follow": "關注你:", - "notifications.column_settings.mention": "提及你:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.push_meta": "This device", - "notifications.column_settings.reblog": "轉推你的文章:", + "notifications.column_settings.favourite": "收藏了你的文章:", + "notifications.column_settings.follow": "關注你:", + "notifications.column_settings.mention": "提及你:", + "notifications.column_settings.push": "推送通知", + "notifications.column_settings.push_meta": "這臺設備", + "notifications.column_settings.reblog": "轉推你的文章:", "notifications.column_settings.show": "在通知欄顯示", "notifications.column_settings.sound": "播放音效", "onboarding.done": "開始使用", @@ -161,17 +163,17 @@ "report.target": "舉報", "search.placeholder": "搜尋", "search_results.total": "{count, number} 項結果", - "standalone.public_title": "A look inside...", + "standalone.public_title": "站點一瞥…", "status.cannot_reblog": "這篇文章無法被轉推", "status.delete": "刪除", - "status.embed": "Embed", - "status.favourite": "喜歡", + "status.embed": "鑲嵌", + "status.favourite": "收藏", "status.load_more": "載入更多", "status.media_hidden": "隱藏媒體內容", "status.mention": "提及 @{name}", - "status.mute_conversation": "Mute conversation", + "status.mute_conversation": "靜音對話", "status.open": "展開文章", - "status.pin": "Pin on profile", + "status.pin": "置頂到資料頁", "status.reblog": "轉推", "status.reblogged_by": "{name} 轉推", "status.reply": "回應", @@ -182,8 +184,8 @@ "status.share": "Share", "status.show_less": "減少顯示", "status.show_more": "顯示更多", - "status.unmute_conversation": "Unmute conversation", - "status.unpin": "Unpin from profile", + "status.unmute_conversation": "解禁對話", + "status.unpin": "解除置頂", "tabs_bar.compose": "撰寫", "tabs_bar.federated_timeline": "跨站", "tabs_bar.home": "主頁", @@ -193,6 +195,15 @@ "upload_button.label": "上載媒體檔案", "upload_form.undo": "還原", "upload_progress.label": "上載中……", + "video.close": "關閉影片", + "video.exit_fullscreen": "退出全熒幕", + "video.expand": "展開影片", + "video.fullscreen": "全熒幕", + "video.hide": "隱藏影片", + "video.mute": "靜音", + "video.pause": "暫停", + "video.play": "播放", + "video.unmute": "解除靜音", "video_player.expand": "展開影片", "video_player.toggle_sound": "開關音效", "video_player.toggle_visible": "打開或關上", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index dcb9d7f3c..b3cc6add7 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -1,11 +1,11 @@ { "account.block": "封鎖 @{name}", - "account.block_domain": "隱藏來自 {domain} 的一切", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", - "account.edit_profile": "編輯用戶資訊", + "account.block_domain": "隱藏來自 {domain} 的一切貼文", + "account.disclaimer_full": "下列資料不一定完整。", + "account.edit_profile": "編輯用者資訊", "account.follow": "關注", "account.followers": "專注者", - "account.follows": "正在關注", + "account.follows": "正關注", "account.follows_you": "關注你", "account.media": "媒體", "account.mention": "提到 @{name}", @@ -13,19 +13,19 @@ "account.posts": "貼文", "account.report": "檢舉 @{name}", "account.requested": "正在等待許可", - "account.share": "Share @{name}'s profile", + "account.share": "分享 @{name} 的用者資訊", "account.unblock": "取消封鎖 @{name}", "account.unblock_domain": "不再隱藏 {domain}", "account.unfollow": "取消關注", "account.unmute": "不再消音 @{name}", - "account.view_full_profile": "View full profile", + "account.view_full_profile": "查看完整資訊", "boost_modal.combo": "下次你可以按 {combo} 來跳過", - "bundle_column_error.body": "Something went wrong while loading this component.", - "bundle_column_error.retry": "Try again", - "bundle_column_error.title": "Network error", - "bundle_modal_error.close": "Close", - "bundle_modal_error.message": "Something went wrong while loading this component.", - "bundle_modal_error.retry": "Try again", + "bundle_column_error.body": "加載本組件出錯。", + "bundle_column_error.retry": "重試", + "bundle_column_error.title": "網路錯誤", + "bundle_modal_error.close": "關閉", + "bundle_modal_error.message": "加載本組件出錯。", + "bundle_modal_error.retry": "重試", "column.blocks": "封鎖的使用者", "column.community": "本地時間軸", "column.favourites": "最愛", @@ -33,21 +33,22 @@ "column.home": "家", "column.mutes": "消音的使用者", "column.notifications": "通知", + "column.pins": "置頂貼文", "column.public": "聯盟時間軸", "column_back_button.label": "上一頁", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", - "column_header.pin": "Pin", - "column_header.show_settings": "Show settings", - "column_header.unpin": "Unpin", + "column_header.hide_settings": "隱藏設定", + "column_header.moveLeft_settings": "將欄左移", + "column_header.moveRight_settings": "將欄右移", + "column_header.pin": "置頂", + "column_header.show_settings": "顯示設定", + "column_header.unpin": "撤頂", "column_subheading.navigation": "瀏覽", "column_subheading.settings": "設定", "compose_form.lock_disclaimer": "你的帳號沒有{locked}。任何人都可以關注你,看到發給關注者的貼文。", "compose_form.lock_disclaimer.lock": "上鎖", "compose_form.placeholder": "在想些什麼?", "compose_form.privacy_disclaimer": "你的貼文會被傳到 {domains} 上被提到的使用者。你信任 {domainsCount, plural, one {這個伺服器} other {這些伺服器}}嗎?貼文的隱私設定只會在 Mastodon 副本上生效。如果 {domains} {domainsCount, plural, one {不是一個 Mastodon 副本} other {都不是 Mastodon 副本}},就不會被標記為非公開貼文,而且可能會被轉推或是讓不預期的人看見。", - "compose_form.publish": "推", + "compose_form.publish": "貼掉", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "將此媒體標為敏感", "compose_form.spoiler": "將訊息隱藏在警告訊息之後", @@ -58,13 +59,13 @@ "confirmations.delete.confirm": "刪除", "confirmations.delete.message": "你確定要刪除這個狀態?", "confirmations.domain_block.confirm": "隱藏整個網域", - "confirmations.domain_block.message": "你真的真的確定要封鎖整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。", + "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。", "confirmations.mute.confirm": "消音", "confirmations.mute.message": "你確定要消音 {name} ?", - "confirmations.unfollow.confirm": "Unfollow", - "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.preview": "Here is what it will look like:", + "confirmations.unfollow.confirm": "取消關注", + "confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?", + "embed.instructions": "要內嵌此貼文,請將以下代碼貼進你的網站。", + "embed.preview": "看上去會變成這樣:", "emoji_button.activity": "活動", "emoji_button.flags": "旗幟", "emoji_button.food": "食物與飲料", @@ -72,12 +73,12 @@ "emoji_button.nature": "自然", "emoji_button.objects": "物件", "emoji_button.people": "人", - "emoji_button.search": "搜尋...", + "emoji_button.search": "搜尋…", "emoji_button.symbols": "符號", "emoji_button.travel": "旅遊與地點", "empty_column.community": "本地時間軸是空的。公開寫點什麼吧!", "empty_column.hashtag": "這個主題標籤下什麼都沒有。", - "empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用戶。", + "empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用者。", "empty_column.home.inactivity": "你家的訊息摘要是空的。如果你很久沒活動了,很快它就會重新產生。", "empty_column.home.public_timeline": "公開時間軸", "empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。", @@ -96,22 +97,23 @@ "home.column_settings.show_replies": "顯示回應", "home.settings": "欄位設定", "lightbox.close": "關閉", - "lightbox.next": "Next", - "lightbox.previous": "Previous", + "lightbox.next": "繼續", + "lightbox.previous": "回退", "loading_indicator.label": "讀取中...", "media_gallery.toggle_visible": "切換可見性", "missing_indicator.label": "找不到", "navigation_bar.blocks": "封鎖的使用者", "navigation_bar.community_timeline": "本地時間軸", - "navigation_bar.edit_profile": "編輯用戶資訊", + "navigation_bar.edit_profile": "編輯用者資訊", "navigation_bar.favourites": "最愛", "navigation_bar.follow_requests": "關注請求", "navigation_bar.info": "關於本站", "navigation_bar.logout": "登出", "navigation_bar.mutes": "消音的使用者", + "navigation_bar.pins": "置頂貼文", "navigation_bar.preferences": "偏好設定", "navigation_bar.public_timeline": "聯盟時間軸", - "notification.favourite": "{name}喜歡你的狀態", + "notification.favourite": "{name}收藏了你的狀態", "notification.follow": "{name}關注了你", "notification.mention": "{name}提到了你", "notification.reblog": "{name}推了你的狀態", @@ -121,8 +123,8 @@ "notifications.column_settings.favourite": "最愛:", "notifications.column_settings.follow": "新的關注者:", "notifications.column_settings.mention": "提到:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.push_meta": "This device", + "notifications.column_settings.push": "推送通知", + "notifications.column_settings.push_meta": "這臺設備", "notifications.column_settings.reblog": "轉推:", "notifications.column_settings.show": "顯示在欄位中", "notifications.column_settings.sound": "播放音效", @@ -135,8 +137,8 @@ "onboarding.page_one.handle": "你在 {domain} 上,所以你的帳號全名是 {handle}", "onboarding.page_one.welcome": "歡迎來到 Mastodon !", "onboarding.page_six.admin": "你的副本的管理員是 {admin} 。", - "onboarding.page_six.almost_done": "快好了...", - "onboarding.page_six.appetoot": "推口大開!", + "onboarding.page_six.almost_done": "快好了…", + "onboarding.page_six.appetoot": "貼口大開!", "onboarding.page_six.apps_available": "在 iOS 、 Android 和其他平台上有這些 {apps} 可以用。", "onboarding.page_six.github": "Mastodon 是自由的開源軟體。你可以在 {github} 上回報臭蟲、請求新功能或是做出貢獻。", "onboarding.page_six.guidelines": "社群指南", @@ -161,17 +163,17 @@ "report.target": "通報中", "search.placeholder": "搜尋", "search_results.total": "{count, number} 項結果", - "standalone.public_title": "A look inside...", + "standalone.public_title": "站點一瞥…", "status.cannot_reblog": "此貼文無法轉推", "status.delete": "刪除", "status.embed": "Embed", - "status.favourite": "喜愛", + "status.favourite": "收藏", "status.load_more": "載入更多", "status.media_hidden": "媒體已隱藏", "status.mention": "提到 @{name}", "status.mute_conversation": "消音對話", "status.open": "展開這個狀態", - "status.pin": "Pin on profile", + "status.pin": "置頂到個人資訊頁", "status.reblog": "轉推", "status.reblogged_by": "{name} 轉推了", "status.reply": "回應", @@ -183,7 +185,7 @@ "status.show_less": "看少點", "status.show_more": "看更多", "status.unmute_conversation": "不消音對話", - "status.unpin": "Unpin from profile", + "status.unpin": "解除置頂", "tabs_bar.compose": "編輯", "tabs_bar.federated_timeline": "聯盟", "tabs_bar.home": "家", @@ -193,6 +195,15 @@ "upload_button.label": "增加媒體", "upload_form.undo": "復原", "upload_progress.label": "上傳中...", + "video.close": "關閉影片", + "video.exit_fullscreen": "退出全熒幕", + "video.expand": "展開影片", + "video.fullscreen": "全熒幕", + "video.hide": "隱藏影片", + "video.mute": "消音", + "video.pause": "暫停", + "video.play": "播放", + "video.unmute": "解除消音", "video_player.expand": "展開影片", "video_player.toggle_sound": "切換音效", "video_player.toggle_visible": "切換可見性", diff --git a/app/javascript/mastodon/reducers/height_cache.js b/app/javascript/mastodon/reducers/height_cache.js new file mode 100644 index 000000000..2f5716fae --- /dev/null +++ b/app/javascript/mastodon/reducers/height_cache.js @@ -0,0 +1,23 @@ +import { Map as ImmutableMap } from 'immutable'; +import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from '../actions/height_cache'; + +const initialState = ImmutableMap(); + +const setHeight = (state, key, id, height) => { + return state.update(key, ImmutableMap(), map => map.set(id, height)); +}; + +const clearHeights = () => { + return ImmutableMap(); +}; + +export default function statuses(state = initialState, action) { + switch(action.type) { + case HEIGHT_CACHE_SET: + return setHeight(state, action.key, action.id, action.height); + case HEIGHT_CACHE_CLEAR: + return clearHeights(); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index a54fca530..444a20845 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -21,6 +21,7 @@ import compose from './compose'; import search from './search'; import media_attachments from './media_attachments'; import notifications from './notifications'; +import height_cache from './height_cache'; const reducers = { timelines, @@ -45,6 +46,7 @@ const reducers = { search, media_attachments, notifications, + height_cache, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index eec2a5f16..38b23504e 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -15,8 +15,6 @@ import { CONTEXT_FETCH_SUCCESS, STATUS_MUTE_SUCCESS, STATUS_UNMUTE_SUCCESS, - STATUS_SET_HEIGHT, - STATUSES_CLEAR_HEIGHT, } from '../actions/statuses'; import { TIMELINE_REFRESH_SUCCESS, @@ -60,9 +58,14 @@ const normalizeStatus = (state, status) => { } const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji.url; + return obj; + }, {}); + normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; - normalStatus.contentHtml = emojify(normalStatus.content); - normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || '')); + normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap); return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); }; @@ -95,18 +98,6 @@ const filterStatuses = (state, relationship) => { return state; }; -const setHeight = (state, id, height) => { - return state.update(id, ImmutableMap(), map => map.set('height', height)); -}; - -const clearHeights = (state) => { - state.forEach(status => { - state = state.deleteIn([status.get('id'), 'height']); - }); - - return state; -}; - const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { @@ -148,10 +139,6 @@ export default function statuses(state = initialState, action) { return deleteStatus(state, action.id, action.references); case ACCOUNT_BLOCK_SUCCESS: return filterStatuses(state, action.relationship); - case STATUS_SET_HEIGHT: - return setHeight(state, action.id, action.height); - case STATUSES_CLEAR_HEIGHT: - return clearHeights(state); default: return state; } diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 8b201ecf8..8842d6dcb 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -25,6 +25,11 @@ function main() { const emojify = require('../mastodon/emoji').default; const { getLocale } = require('../mastodon/locales'); const { localeData } = getLocale(); + const VideoContainer = require('../mastodon/containers/video_container').default; + const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default; + const CardContainer = require('../mastodon/containers/card_container').default; + const React = require('react'); + const ReactDOM = require('react-dom'); localeData.forEach(IntlRelativeFormat.__addLocaleData); @@ -66,22 +71,21 @@ function main() { window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes'); }); }); - }); - delegate(document, '.video-player video', 'click', ({ target }) => { - if (target.paused) { - target.play(); - } else { - target.pause(); - } - }); + [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => { + const props = JSON.parse(content.getAttribute('data-props')); + ReactDOM.render(<VideoContainer locale={locale} {...props} />, content); + }); - delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() { - this.parentNode.classList.add('media-spoiler-wrapper__visible'); - }); + [].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => { + const props = JSON.parse(content.getAttribute('data-props')); + ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content); + }); - delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() { - this.parentNode.classList.remove('media-spoiler-wrapper__visible'); + [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => { + const props = JSON.parse(content.getAttribute('data-props')); + ReactDOM.render(<CardContainer locale={locale} {...props} />, content); + }); }); delegate(document, '.webapp-btn', 'click', ({ target, button }) => { @@ -126,7 +130,7 @@ function main() { delegate(document, '#account_avatar', 'change', ({ target }) => { const avatar = document.querySelector('.card.compact .avatar img'); const [file] = target.files || []; - const url = URL.createObjectURL(file); + const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc; avatar.src = url; }); @@ -134,7 +138,7 @@ function main() { delegate(document, '#account_header', 'change', ({ target }) => { const header = document.querySelector('.card.compact'); const [file] = target.files || []; - const url = URL.createObjectURL(file); + const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc; header.style.backgroundImage = `url(${url})`; }); diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss index 28924738a..343de1590 100644 --- a/app/javascript/styles/about.scss +++ b/app/javascript/styles/about.scss @@ -137,7 +137,7 @@ padding-bottom: 15px; .hero .heading { - padding-bottom: 30px; + padding-bottom: 20px; font-family: 'mastodon-font-sans-serif', sans-serif; font-size: 16px; font-weight: 400; @@ -327,7 +327,7 @@ .about-short { background: darken($ui-base-color, 4%); - padding: 50px 0; + padding: 50px 0 30px; font-family: 'mastodon-font-sans-serif', sans-serif; font-size: 16px; font-weight: 400; @@ -640,8 +640,11 @@ .header-wrapper { padding-top: 0; + &.compact { + padding-bottom: 0; + } + &.compact .hero .heading { - padding-bottom: 20px; text-align: initial; } } diff --git a/app/javascript/styles/admin.scss b/app/javascript/styles/admin.scss index fa7859e38..87bc710af 100644 --- a/app/javascript/styles/admin.scss +++ b/app/javascript/styles/admin.scss @@ -97,6 +97,14 @@ margin-bottom: 40px; } + h3 { + color: $ui-secondary-color; + font-size: 20px; + line-height: 28px; + font-weight: 400; + margin-bottom: 30px; + } + h6 { font-size: 16px; color: $ui-secondary-color; diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 3cbfb7d3a..a6e5946a7 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -631,6 +631,10 @@ opacity: 1; animation: fade 150ms linear; + .video-player { + margin-top: 8px; + } + &.status-direct { background: lighten($ui-base-color, 8%); @@ -867,6 +871,10 @@ height: 22px; } } + + .video-player { + margin-top: 8px; + } } .detailed-status__meta { @@ -1610,9 +1618,8 @@ .column, .drawer { - @supports(display: grid) { // hack to fix Chrome <57 - contain: strict; - } + flex: 1 1 100%; + overflow: hidden; } @include limited-single-column('screen and (max-width: 360px)', $parent: null) { @@ -1790,9 +1797,7 @@ overflow-x: hidden; flex: 1 1 auto; -webkit-overflow-scrolling: touch; - @supports(display: grid) { // hack to fix Chrome <57 - contain: strict; - } + will-change: transform; // improves perf in mobile Chrome &.optionally-scrollable { overflow-y: auto; @@ -2642,7 +2647,7 @@ button.icon-button.active i.fa-retweet { .media-spoiler { background: $base-overlay-background; - color: $primary-text-color; + color: $ui-primary-color; border: 0; width: 100%; height: 100%; @@ -4206,6 +4211,182 @@ button.icon-button.active i.fa-retweet { z-index: 5; } +.video-player { + overflow: hidden; + position: relative; + background: $base-shadow-color; + max-width: 100%; + + video { + height: 100%; + width: 100%; + z-index: 1; + } + + &.fullscreen { + width: 100% !important; + height: 100% !important; + margin: 0; + + video { + max-width: 100% !important; + max-height: 100% !important; + } + } + + &.inline { + video { + object-fit: cover; + position: relative; + top: 50%; + transform: translateY(-50%); + } + } + + &__controls { + position: absolute; + z-index: 2; + bottom: 0; + left: 0; + right: 0; + box-sizing: border-box; + background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent); + padding: 0 10px; + opacity: 0; + transition: opacity .1s ease; + + &.active { + opacity: 1; + } + } + + &.inactive { + video, + .video-player__controls { + visibility: hidden; + } + } + + &__spoiler { + display: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 4; + border: 0; + background: $base-shadow-color; + color: $ui-primary-color; + transition: none; + pointer-events: none; + + &.active { + display: block; + pointer-events: auto; + + &:hover, + &:active, + &:focus { + color: lighten($ui-primary-color, 8%); + } + } + + &__title { + display: block; + font-size: 14px; + } + + &__subtitle { + display: block; + font-size: 11px; + font-weight: 500; + } + } + + &__buttons { + padding-bottom: 10px; + font-size: 16px; + + &.left { + float: left; + + button { + padding-right: 10px; + } + } + + &.right { + float: right; + + button { + padding-left: 10px; + } + } + + button { + background: transparent; + padding: 0; + border: 0; + color: $white; + + &:active, + &:hover, + &:focus { + color: $ui-highlight-color; + } + } + } + + &__seek { + cursor: pointer; + height: 24px; + position: relative; + + &::before { + content: ""; + width: 100%; + background: rgba($white, 0.35); + display: block; + position: absolute; + height: 4px; + top: 10px; + } + + &__progress { + display: block; + position: absolute; + height: 4px; + top: 10px; + background: $ui-highlight-color; + } + + &__handle { + position: absolute; + z-index: 3; + opacity: 0; + border-radius: 50%; + width: 12px; + height: 12px; + top: 6px; + margin-left: -6px; + transition: opacity .1s ease; + background: $ui-highlight-color; + pointer-events: none; + + &.active { + opacity: 1; + } + } + + &:hover { + .video-player__seek__handle { + opacity: 1; + } + } + } +} + .media-spoiler-video { background-size: cover; background-repeat: no-repeat; diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss index 747610237..0526f174c 100644 --- a/app/javascript/styles/forms.scss +++ b/app/javascript/styles/forms.scss @@ -349,9 +349,46 @@ code { box-shadow: 0 0 5px rgba($base-shadow-color, 0.2); text-align: center; + p { + margin-bottom: 15px; + } + + .oauth-code { + 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; + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus, + &:active { + outline: 0 !important; + } + + &:focus { + background: lighten($ui-base-color, 4%); + } + } + strong { font-weight: 500; } + + @media screen and (max-width: 740px) and (min-width: 441px) { + margin-top: 40px; + } } .form-footer { diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss index 35225c045..453070b7c 100644 --- a/app/javascript/styles/stream_entries.scss +++ b/app/javascript/styles/stream_entries.scss @@ -140,19 +140,6 @@ } } } - - .status__attachments { - margin-top: 8px; - overflow: hidden; - width: 100%; - box-sizing: border-box; - position: relative; - - .status__attachments__inner { - display: flex; - height: 214px; - } - } } .detailed-status.light { @@ -233,139 +220,35 @@ } } - .detailed-status__attachments { - margin-top: 8px; - overflow: hidden; - width: 100%; - box-sizing: border-box; - position: relative; + .status-card { + border-color: lighten($ui-secondary-color, 4%); + color: darken($ui-primary-color, 4%); - .status__attachments__inner { - display: flex; - height: 360px; + &:hover { + background: lighten($ui-secondary-color, 4%); } } - .video-player { - margin-top: 8px; - height: 300px; - overflow: hidden; - position: relative; - - video { - position: relative; - z-index: 1; - width: 100%; - height: 100%; - object-fit: cover; - top: 50%; - transform: translateY(-50%); - } - } - } - - .media-item, - .video-item { - box-sizing: border-box; - position: relative; - left: auto; - top: auto; - right: auto; - bottom: auto; - float: left; - border: medium none; - display: block; - flex: 1 1 auto; - width: 100%; - height: 100%; - overflow: hidden; - margin-right: 2px; - - &:last-child { - margin-right: 0; - } - - a { - display: block; - width: 100%; - height: 100%; - background: no-repeat scroll center center / cover; - text-decoration: none; - cursor: zoom-in; - } - - video { - position: relative; - z-index: 1; - width: 100%; - height: 100%; - object-fit: cover; - top: 50%; - transform: translateY(-50%); - } - } - - .video-item { - a { - cursor: pointer; + .status-card__title, + .status-card__description { + color: $ui-base-color; } - .video-item__play { - position: absolute; - top: 50%; - left: 50%; - font-size: 36px; - transform: translate(-50%, -50%); - padding: 5px; - border-radius: 100px; - color: rgba($primary-text-color, 0.8); - z-index: 1; + .status-card__image { + background: $ui-secondary-color; } } .media-spoiler { background: $ui-primary-color; - width: 100%; - height: 100%; - cursor: pointer; - position: absolute; - top: 0; - left: 0; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - text-align: center; + color: $white; transition: all 100ms linear; - z-index: 2; - &:hover { + &:hover, + &:active, + &:focus { background: darken($ui-primary-color, 5%); - } - - span { - display: block; - - &:first-child { - font-size: 14px; - } - - &:last-child { - font-size: 11px; - font-weight: 500; - } - } - } - - .media-spoiler-wrapper { - &.media-spoiler-wrapper__visible { - .media-spoiler { - display: none; - } - - .spoiler-button { - display: block; - } + color: unset; } } diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index c4da405c7..556f91235 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -11,7 +11,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity return status unless status.nil? - status = Status.create!(account: @account, reblog: original_status, uri: @json['id']) + status = Status.create!( + account: @account, + reblog: original_status, + uri: @json['id'], + created_at: @json['published'] || Time.now.utc + ) distribute(status) status end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 9a34484f5..41f2b0bad 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -4,26 +4,31 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def perform return if delete_arrived_first?(object_uri) || unsupported_object_type? - status = find_existing_status + RedisLock.acquire(lock_options) do |lock| + if lock.acquired? + @status = find_existing_status + process_status if @status.nil? + end + end + + @status + end - return status unless status.nil? + private + def process_status ApplicationRecord.transaction do - status = Status.create!(status_params) + @status = Status.create!(status_params) - process_tags(status) - process_attachments(status) + process_tags(@status) + process_attachments(@status) end - resolve_thread(status) - distribute(status) - forward_for_reply if status.public_visibility? || status.unlisted_visibility? - - status + resolve_thread(@status) + distribute(@status) + forward_for_reply if @status.public_visibility? || @status.unlisted_visibility? end - private - def find_existing_status status = status_from_uri(object_uri) status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present? @@ -56,6 +61,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity process_hashtag tag, status when 'Mention' process_mention tag, status + when 'Emoji' + process_emoji tag, status end end end @@ -74,6 +81,17 @@ class ActivityPub::Activity::Create < ActivityPub::Activity account.mentions.create(status: status) end + def process_emoji(tag, _status) + shortcode = tag['name'].delete(':') + emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain) + + return if !emoji.nil? || skip_download? + + emoji = CustomEmoji.new(domain: @account.domain, shortcode: shortcode) + emoji.image_remote_url = tag['href'] + emoji.save + end + def process_attachments(status) return unless @object['attachment'].is_a?(Array) @@ -182,4 +200,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return unless @json['signature'].present? && reply_to_local? ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id) end + + def lock_options + { redis: Redis.current, key: "create:#{@object['id']}" } + end end diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 6ed66a239..790d2025c 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -14,6 +14,8 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base 'atomUri' => 'ostatus:atomUri', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation', + 'toot' => 'http://joinmastodon.org/ns#', + 'Emoji' => 'toot:Emoji', }, ], }.freeze @@ -28,7 +30,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base def serializable_hash(options = nil) options = serialization_options(options) - serialized_hash = CONTEXT.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) - self.class.transform_key_casing!(serialized_hash, instance_options) + serialized_hash = ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options) + CONTEXT.merge(self.class.transform_key_casing!(serialized_hash, instance_options)) end end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 929e87852..1b4e271db 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -37,7 +37,7 @@ class ActivityPub::TagManager end def activity_uri_for(target) - return nil unless %i(note comment activity).include?(target.object_type) && target.local? + raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? activity_account_status_url(target.account, target) end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index cacc0364f..29fea27de 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -9,7 +9,7 @@ class Formatter include ActionView::Helpers::TextHelper - def format(status) + def format(status, options = {}) if status.reblog? prepend_reblog = status.reblog.account.acct status = status.proper @@ -19,7 +19,11 @@ class Formatter raw_content = status.text - return reformat(raw_content) unless status.local? + unless status.local? + html = reformat(raw_content) + html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify] + return html + end linkable_accounts = status.mentions.map(&:account) linkable_accounts << status.account @@ -27,6 +31,7 @@ class Formatter html = raw_content html = "RT @#{prepend_reblog} #{html}" if prepend_reblog html = encode_and_link_urls(html, linkable_accounts) + html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify] html = simple_format(html, {}, sanitize: false) html = html.delete("\n") @@ -39,7 +44,9 @@ class Formatter def plaintext(status) return status.text if status.local? - strip_tags(status.text) + + text = status.text.gsub(/(<br \/>|<br>|<\/p>)+/) { |match| "#{match}\n" } + strip_tags(text) end def simplified_format(account) @@ -76,6 +83,47 @@ class Formatter end end + def encode_custom_emojis(html, emojis) + return html if emojis.empty? + + emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h + + i = -1 + inside_tag = false + inside_shortname = false + shortname_start_index = -1 + + while i + 1 < html.size + i += 1 + + if inside_shortname && html[i] == ':' + shortcode = html[shortname_start_index + 1..i - 1] + emoji = emoji_map[shortcode] + + if emoji + replacement = "<img draggable=\"false\" class=\"emojione\" alt=\":#{shortcode}:\" title=\":#{shortcode}:\" src=\"#{emoji}\" />" + before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : '' + html = before_html + replacement + html[i + 1..-1] + i += replacement.size - (shortcode.size + 2) - 1 + else + i -= 1 + end + + inside_shortname = false + elsif inside_tag && html[i] == '>' + inside_tag = false + elsif html[i] == '<' + inside_tag = true + inside_shortname = false + elsif !inside_tag && html[i] == ':' + inside_shortname = true + shortname_start_index = i + end + end + + html + end + def rewrite(text, entities) chars = text.to_s.to_char_a @@ -131,13 +179,13 @@ class Formatter end def link_html(url) - url = Addressable::URI.parse(url).display_uri.to_s + url = Addressable::URI.parse(url).to_s prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s text = url[prefix.length, 30] suffix = url[prefix.length + 30..-1] cutoff = url[prefix.length..-1].length > 30 - "<span class=\"invisible\">#{prefix}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{text}</span><span class=\"invisible\">#{suffix}</span>" + "<span class=\"invisible\">#{encode(prefix)}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{encode(text)}</span><span class=\"invisible\">#{encode(suffix)}</span>" end def hashtag_html(tag) diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb index 1d9932b52..a42460e10 100644 --- a/app/lib/language_detector.rb +++ b/app/lib/language_detector.rb @@ -1,26 +1,31 @@ # frozen_string_literal: true class LanguageDetector - attr_reader :text, :account + include Singleton - def initialize(text, account = nil) - @text = text - @account = account + def initialize @identifier = CLD3::NNetLanguageIdentifier.new(1, 2048) end - def to_iso_s - detected_language_code || default_locale + def detect(text, account) + detect_language_code(text) || default_locale(account) end - def prepared_text - simplified_text.strip + def language_names + @language_names = + CLD3::TaskContextParams::LANGUAGE_NAMES.map { |name| iso6391(name.to_s).to_sym } + .uniq end private - def detected_language_code - iso6391(result.language).to_sym if detected_language_reliable? + def prepare_text(text) + simplify_text(text).strip + end + + def detect_language_code(text) + result = @identifier.find_language(prepare_text(text)) + iso6391(result.language.to_s).to_sym if result.reliable? end def iso6391(bcp47) @@ -32,15 +37,7 @@ class LanguageDetector ISO_639.find(iso639).alpha2 end - def result - @result ||= @identifier.find_language(prepared_text) - end - - def detected_language_reliable? - result.reliable? - end - - def simplified_text + def simplify_text(text) text.dup.tap do |new_text| new_text.gsub!(FetchLinkCardService::URL_PATTERN, '') new_text.gsub!(Account::MENTION_RE, '') @@ -49,7 +46,7 @@ class LanguageDetector end end - def default_locale - account&.user_locale&.to_sym || nil + def default_locale(account) + account.user_locale&.to_sym end end diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index 1a23c9efa..d3f1629c4 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -42,6 +42,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base save_mentions(status) save_hashtags(status) save_media(status) + save_emojis(status) end if thread? && status.thread.nil? @@ -150,6 +151,25 @@ class OStatus::Activity::Creation < OStatus::Activity::Base end end + def save_emojis(parent) + do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? + + return if do_not_download + + @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: TagManager::XMLNS).each do |link| + next unless link['href'] && link['name'] + + shortcode = link['name'].delete(':') + emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain) + + next unless emoji.nil? + + emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain) + emoji.image_remote_url = link['href'] + emoji.save + end + end + def account_from_href(href) url = Addressable::URI.parse(href).normalize diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index b8e22a381..a6a5cb0c4 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -368,5 +368,9 @@ class OStatus::AtomSerializer end append_element(entry, 'mastodon:scope', status.visibility) + + status.emojis.each do |emoji| + append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode) + end end end diff --git a/app/lib/request.rb b/app/lib/request.rb index c01e07925..b083edaf7 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -31,6 +31,8 @@ class Request def perform http_client.headers(headers).public_send(@verb, @url.to_s, @options) + rescue => e + raise e.class, "#{e.message} on #{@url}" end def headers diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index f33a20c6f..1d0a24e42 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -87,7 +87,7 @@ class TagManager def local_url?(url) uri = Addressable::URI.parse(url).normalize domain = uri.host + (uri.port ? ":#{uri.port}" : '') - TagManager.instance.local_domain?(domain) + TagManager.instance.web_domain?(domain) end def uri_for(target) diff --git a/app/models/account.rb b/app/models/account.rb index ac27c7923..1b996e3cc 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -106,6 +106,7 @@ class Account < ApplicationRecord scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') } scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } + scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } delegate :email, :current_sign_in_ip, @@ -174,6 +175,10 @@ class Account < ApplicationRecord end class << self + def readonly_attributes + super - %w(statuses_count following_count followers_count) + end + def domains reorder(nil).pluck('distinct accounts.domain') end diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 270043a9e..990035b34 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -27,9 +27,11 @@ module Remotable matches = response.headers['content-disposition']&.match(/filename="([^"]*)"/) filename = matches.nil? ? parsed_url.path.split('/').last : matches[1] + basename = SecureRandom.hex(8) + extname = File.extname(filename) send("#{attachment_name}=", StringIO.new(response.to_s)) - send("#{attachment_name}_file_name=", filename) + send("#{attachment_name}_file_name=", basename + extname) self[attribute_name] = url if has_attribute?(attribute_name) rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb new file mode 100644 index 000000000..f4d3b16a0 --- /dev/null +++ b/app/models/custom_emoji.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: custom_emojis +# +# id :integer not null, primary key +# shortcode :string default(""), not null +# domain :string +# image_file_name :string +# image_content_type :string +# image_file_size :integer +# image_updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class CustomEmoji < ApplicationRecord + SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}' + + SCAN_RE = /(?<=[^[:alnum:]:]|\n|^) + :(#{SHORTCODE_RE_FRAGMENT}): + (?=[^[:alnum:]:]|$)/x + + has_attached_file :image + + validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes } + validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 } + + include Remotable + + class << self + def from_text(text, domain) + return [] if text.blank? + shortcodes = text.scan(SCAN_RE).map(&:first) + where(shortcode: shortcodes, domain: domain) + end + end +end diff --git a/app/models/instance_filter.rb b/app/models/instance_filter.rb new file mode 100644 index 000000000..5073cf1fa --- /dev/null +++ b/app/models/instance_filter.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class InstanceFilter + attr_reader :params + + def initialize(params) + @params = params + end + + def results + scope = Account.remote.by_domain_accounts + params.each do |key, value| + scope.merge!(scope_for(key, value)) if value.present? + end + scope + end + + private + + def scope_for(key, value) + case key.to_s + when 'domain_name' + Account.matches_domain(value) + else + raise "Unknown filter: #{key}" + end + end +end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index d83ca44f1..d913e7372 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -56,15 +56,21 @@ class MediaAttachment < ApplicationRecord validates :account, presence: true - scope :attached, -> { where.not(status_id: nil) } + scope :attached, -> { where.not(status_id: nil) } scope :unattached, -> { where(status_id: nil) } - scope :local, -> { where(remote_url: '') } + scope :local, -> { where(remote_url: '') } + scope :remote, -> { where.not(remote_url: '') } + default_scope { order(id: :asc) } def local? remote_url.blank? end + def needs_redownload? + file.blank? && remote_url.present? + end + def to_param shortcode end diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb new file mode 100644 index 000000000..8ffdc8313 --- /dev/null +++ b/app/models/site_upload.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: site_uploads +# +# id :integer not null, primary key +# var :string default(""), not null +# file_file_name :string +# file_content_type :string +# file_file_size :integer +# file_updated_at :datetime +# meta :json +# created_at :datetime not null +# updated_at :datetime not null +# + +class SiteUpload < ApplicationRecord + has_attached_file :file + + validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/ + validates :var, presence: true, uniqueness: true + + before_save :set_meta + after_commit :clear_cache + + def cache_key + "site_uploads/#{var}" + end + + private + + def set_meta + tempfile = file.queued_for_write[:original] + + return if tempfile.nil? + + geometry = Paperclip::Geometry.from_file(tempfile) + self.meta = { width: geometry.width.to_i, height: geometry.height.to_i } + end + + def clear_cache + Rails.cache.delete(cache_key) + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 514cab2e4..326d128d6 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -55,7 +55,7 @@ class Status < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy has_one :stream_entry, as: :activity, inverse_of: :status - validates :uri, uniqueness: true, unless: :local? + validates :uri, uniqueness: true, presence: true, unless: :local? validates :text, presence: true, unless: :reblog? validates_with StatusLengthValidator validates :reblog, uniqueness: { scope: :account }, if: :reblog? @@ -70,7 +70,6 @@ class Status < ApplicationRecord scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } scope :with_public_visibility, -> { where(visibility: :public) } scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) } - scope :local_only, -> { left_outer_joins(:account).where(accounts: { domain: nil }) } scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: false }) } scope :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) } scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) } @@ -132,6 +131,10 @@ class Status < ApplicationRecord !sensitive? && media_attachments.any? end + def emojis + CustomEmoji.from_text(text, account.domain) + end + after_create :store_uri, if: :local? before_validation :prepare_contents, if: :local? @@ -221,7 +224,7 @@ class Status < ApplicationRecord private def timeline_scope(local_only = false) - starting_scope = local_only ? Status.local_only : Status + starting_scope = local_only ? Status.local : Status starting_scope .with_public_visibility .without_reblogs diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index b1afb9e1f..1c08fb3bc 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -21,7 +21,7 @@ class InstancePresenter end def status_count - Rails.cache.fetch('local_status_count') { Status.local.count } + Rails.cache.fetch('local_status_count') { Account.local.sum(:statuses_count) } end def domain_count @@ -44,4 +44,8 @@ class InstancePresenter def source_url Mastodon::Version.source_url end + + def thumbnail + @thumbnail ||= Rails.cache.fetch('site_uploads/thumbnail') { SiteUpload.find_by(var: 'thumbnail') } + end end diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb index 349495e84..b252e008b 100644 --- a/app/serializers/activitypub/activity_serializer.rb +++ b/app/serializers/activitypub/activity_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ActivityPub::ActivitySerializer < ActiveModel::Serializer - attributes :id, :type, :actor, :to, :cc + attributes :id, :type, :actor, :published, :to, :cc has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer @@ -17,6 +17,10 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer ActivityPub::TagManager.instance.uri_for(object.account) end + def published + object.created_at.iso8601 + end + def to ActivityPub::TagManager.instance.to(object) end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index d42f54263..e5d8e3f03 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -27,7 +27,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end def in_reply_to - return unless object.reply? + return unless object.reply? && !object.thread.nil? if object.thread.uri.nil? || object.thread.uri.start_with?('http') ActivityPub::TagManager.instance.uri_for(object.thread) @@ -57,7 +57,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end def virtual_tags - object.mentions + object.tags + object.mentions + object.tags + object.emojis end def atom_uri @@ -67,12 +67,14 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end def in_reply_to_atom_uri - return unless object.reply? + return unless object.reply? && !object.thread.nil? ::TagManager.instance.uri_for(object.thread) end def conversation + return if object.conversation.nil? + if object.conversation.uri? object.conversation.uri else @@ -135,4 +137,22 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer "##{object.name}" end end + + class CustomEmojiSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :type, :href, :name + + def type + 'Emoji' + end + + def href + full_asset_url(object.image.url) + end + + def name + ":#{object.shortcode}:" + end + end end diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb index af03fd47a..0c8350e2d 100644 --- a/app/serializers/oembed_serializer.rb +++ b/app/serializers/oembed_serializer.rb @@ -45,7 +45,7 @@ class OEmbedSerializer < ActiveModel::Serializer height: height, } - content_tag(:iframe, nil, attributes) + content_tag(:script, nil, src: full_asset_url('embed.js'), async: true) + content_tag(:iframe, nil, attributes) + content_tag(:script, nil, src: full_asset_url('embed.js', skip_pipeline: true), async: true) end def width diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index a97137909..2898011fd 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class REST::InstanceSerializer < ActiveModel::Serializer + include RoutingHelper + attributes :uri, :title, :description, :email, - :version, :urls, :stats + :version, :urls, :stats, :thumbnail def uri Rails.configuration.x.local_domain @@ -24,6 +26,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer Mastodon::Version.to_s end + def thumbnail + full_asset_url(instance_presenter.thumbnail.file.url) if instance_presenter.thumbnail + end + def stats { user_count: instance_presenter.user_count, diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb index 9055b8db4..31189406a 100644 --- a/app/serializers/rest/media_attachment_serializer.rb +++ b/app/serializers/rest/media_attachment_serializer.rb @@ -7,11 +7,19 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer :remote_url, :text_url, :meta def url - full_asset_url(object.file.url(:original)) + if object.needs_redownload? + media_proxy_url(object.id, :original) + else + full_asset_url(object.file.url(:original)) + end end def preview_url - full_asset_url(object.file.url(:small)) + if object.needs_redownload? + media_proxy_url(object.id, :small) + else + full_asset_url(object.file.url(:small)) + end end def text_url diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 298a3bb40..d8efa8e60 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -17,6 +17,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :media_attachments, serializer: REST::MediaAttachmentSerializer has_many :mentions has_many :tags + has_many :emojis def current_user? !current_user.nil? @@ -106,4 +107,14 @@ class REST::StatusSerializer < ActiveModel::Serializer tag_url(object) end end + + class CustomEmojiSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :shortcode, :url + + def url + full_asset_url(object.image.url) + end + end end diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 68ca58d62..a95931afe 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -17,6 +17,8 @@ class ActivityPub::FetchRemoteStatusService < BaseService actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account) actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id) if actor.nil? + return if actor.suspended? + ActivityPub::Activity.factory(activity, actor).perform end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index b54e447ad..811209537 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -12,12 +12,21 @@ class ActivityPub::ProcessAccountService < BaseService @uri = @json['id'] @username = username @domain = domain - @account = Account.find_by(uri: @uri) @collections = {} - create_account if @account.nil? - upgrade_account if @account.ostatus? - update_account + RedisLock.acquire(lock_options) do |lock| + if lock.acquired? + @account = Account.find_by(uri: @uri) + @old_public_key = @account&.public_key + @old_protocol = @account&.protocol + + create_account if @account.nil? + update_account + end + end + + after_protocol_change! if protocol_changed? + after_key_change! if key_changed? @account rescue Oj::ParseError @@ -35,33 +44,46 @@ class ActivityPub::ProcessAccountService < BaseService @account.suspended = true if auto_suspend? @account.silenced = true if auto_silence? @account.private_key = nil - @account.save! end def update_account @account.last_webfingered_at = Time.now.utc @account.protocol = :activitypub - @account.inbox_url = @json['inbox'] || '' - @account.outbox_url = @json['outbox'] || '' - @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || '' - @account.followers_url = @json['followers'] || '' - @account.url = url || @uri - @account.display_name = @json['name'] || '' - @account.note = @json['summary'] || '' - @account.avatar_remote_url = image_url('icon') unless skip_download? - @account.header_remote_url = image_url('image') unless skip_download? - @account.public_key = public_key || '' - @account.locked = @json['manuallyApprovesFollowers'] || false - @account.statuses_count = outbox_total_items if outbox_total_items.present? - @account.following_count = following_total_items if following_total_items.present? - @account.followers_count = followers_total_items if followers_total_items.present? + + set_immediate_attributes! + set_fetchable_attributes! + @account.save_with_optional_media! end - def upgrade_account + def set_immediate_attributes! + @account.inbox_url = @json['inbox'] || '' + @account.outbox_url = @json['outbox'] || '' + @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || '' + @account.followers_url = @json['followers'] || '' + @account.url = url || @uri + @account.display_name = @json['name'] || '' + @account.note = @json['summary'] || '' + @account.locked = @json['manuallyApprovesFollowers'] || false + end + + def set_fetchable_attributes! + @account.avatar_remote_url = image_url('icon') unless skip_download? + @account.header_remote_url = image_url('image') unless skip_download? + @account.public_key = public_key || '' + @account.statuses_count = outbox_total_items if outbox_total_items.present? + @account.following_count = following_total_items if following_total_items.present? + @account.followers_count = followers_total_items if followers_total_items.present? + end + + def after_protocol_change! ActivityPub::PostUpgradeWorker.perform_async(@account.domain) end + def after_key_change! + RefollowWorker.perform_async(@account.id) + end + def image_url(key) value = first_of_value(@json[key]) @@ -120,15 +142,27 @@ class ActivityPub::ProcessAccountService < BaseService end def auto_suspend? - domain_block && domain_block.suspend? + domain_block&.suspend? end def auto_silence? - domain_block && domain_block.silence? + domain_block&.silence? end def domain_block return @domain_block if defined?(@domain_block) @domain_block = DomainBlock.find_by(domain: @domain) end + + def key_changed? + !@old_public_key.nil? && @old_public_key != @account.public_key + end + + def protocol_changed? + !@old_protocol.nil? && @old_protocol != @account.protocol + end + + def lock_options + { redis: Redis.current, key: "process_account:#{@uri}" } + end end diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index bc04c50ba..0c6736a3f 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -7,9 +7,9 @@ class ActivityPub::ProcessCollectionService < BaseService @account = account @json = Oj.load(body, mode: :strict) - return if @account.suspended? || !supported_context? - + return unless supported_context? return if different_actor? && verify_account!.nil? + return if @account.suspended? case @json['type'] when 'Collection', 'CollectionPage' diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 1473bc841..eefdc0dbf 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -26,6 +26,7 @@ class BlockDomainService < BaseService def clear_media! clear_account_images clear_account_attachments + clear_emojos end def suspend_accounts! @@ -51,6 +52,10 @@ class BlockDomainService < BaseService end end + def clear_emojos + emojis_from_blocked_domains.destroy_all + end + def blocked_domain domain_block.domain end @@ -62,4 +67,8 @@ class BlockDomainService < BaseService def media_from_blocked_domain MediaAttachment.joins(:account).merge(blocked_domain_accounts).reorder(nil) end + + def emojis_from_blocked_domains + CustomEmoji.where(domain: blocked_domain) + end end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index c38e9e7df..4acbfae7a 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -1,7 +1,15 @@ # frozen_string_literal: true class FetchLinkCardService < BaseService - URL_PATTERN = %r{https?://\S+} + URL_PATTERN = %r{ + ( # $1 URL + (https?:\/\/)? # $2 Protocol (optional) + (#{Twitter::Regex[:valid_domain]}) # $3 Domain(s) + (?::(#{Twitter::Regex[:valid_port_number]}))? # $4 Port number (optional) + (/#{Twitter::Regex[:valid_url_path]}*)? # $5 URL Path and anchor + (\?#{Twitter::Regex[:valid_url_query_chars]}*#{Twitter::Regex[:valid_url_query_ending_chars]})? # $6 Query String + ) + }iox def call(status) @status = status @@ -14,11 +22,11 @@ class FetchLinkCardService < BaseService RedisLock.acquire(lock_options) do |lock| if lock.acquired? @card = PreviewCard.find_by(url: @url) - process_url if @card.nil? + process_url if @card.nil? || @card.updated_at <= 2.weeks.ago end end - attach_card unless @card.nil? + attach_card if @card&.persisted? rescue HTTP::ConnectionError, OpenSSL::SSL::SSLError nil end @@ -26,8 +34,8 @@ class FetchLinkCardService < BaseService private def process_url - @card = PreviewCard.new(url: @url) - res = Request.new(:head, @url).perform + @card ||= PreviewCard.new(url: @url) + res = Request.new(:head, @url).perform return if res.code != 200 || res.mime_type != 'text/html' @@ -40,7 +48,7 @@ class FetchLinkCardService < BaseService def parse_urls if @status.local? - urls = @status.text.match(URL_PATTERN).to_a.map { |uri| Addressable::URI.parse(uri).normalize } + urls = @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[0]).normalize } else html = Nokogiri::HTML(@status.text) links = html.css('a') @@ -106,12 +114,25 @@ class FetchLinkCardService < BaseService guess = detector.detect(html, response.charset) page = Nokogiri::HTML(html, nil, guess&.fetch(:encoding)) - @card.type = :link - @card.title = meta_property(page, 'og:title') || page.at_xpath('//title')&.content || '' - @card.description = meta_property(page, 'og:description') || meta_property(page, 'description') || '' - @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image') + if meta_property(page, 'twitter:player') + @card.type = :video + @card.width = meta_property(page, 'twitter:player:width') || 0 + @card.height = meta_property(page, 'twitter:player:height') || 0 + @card.html = content_tag(:iframe, nil, src: meta_property(page, 'twitter:player'), + width: @card.width, + height: @card.height, + allowtransparency: 'true', + scrolling: 'no', + frameborder: '0') + else + @card.type = :link + @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image') + end + + @card.title = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || '' + @card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || '' - return if @card.title.blank? + return if @card.title.blank? && @card.html.blank? @card.save_with_optional_image! end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index e5b0fe438..d1b8f42c7 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -28,7 +28,7 @@ class PostStatusService < BaseService sensitive: options[:sensitive], spoiler_text: options[:spoiler_text] || '', visibility: options[:visibility] || account.user&.setting_default_privacy, - language: detect_language_for(text, account), + language: LanguageDetector.instance.detect(text, account), application: options[:application]) attach_media(status, media) @@ -73,10 +73,6 @@ class PostStatusService < BaseService media.update(status_id: status.id) end - def detect_language_for(text, account) - LanguageDetector.new(text, account).to_iso_s - end - def process_mentions_service @process_mentions_service ||= ProcessMentionsService.new end diff --git a/app/services/resolve_remote_account_service.rb b/app/services/resolve_remote_account_service.rb index 7031c98f5..57c80fc82 100644 --- a/app/services/resolve_remote_account_service.rb +++ b/app/services/resolve_remote_account_service.rb @@ -80,6 +80,7 @@ class ResolveRemoteAccountService < BaseService def activitypub_ready? !@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) && + !actor_json.nil? && actor_json['inbox'].present? end diff --git a/app/validators/status_pin_validator.rb b/app/validators/status_pin_validator.rb index f557df6af..9760e1138 100644 --- a/app/validators/status_pin_validator.rb +++ b/app/validators/status_pin_validator.rb @@ -5,5 +5,6 @@ class StatusPinValidator < ActiveModel::Validator pin.errors.add(:status, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog? pin.errors.add(:status, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id pin.errors.add(:status, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility) + pin.errors.add(:status, I18n.t('statuses.pin_errors.limit')) if pin.account.status_pins.count > 4 end end diff --git a/app/views/about/_og.html.haml b/app/views/about/_og.html.haml new file mode 100644 index 000000000..dbd476915 --- /dev/null +++ b/app/views/about/_og.html.haml @@ -0,0 +1,10 @@ +- thumbnail = @instance_presenter.thumbnail += opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname) += opengraph 'og:url', about_url += opengraph 'og:type', 'website' += opengraph 'og:title', @instance_presenter.site_title += opengraph 'og:description', strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon_html')) += opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('preview.jpg', protocol: :request)) += opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200' += opengraph 'og:image:height', thumbnail ? thumbnail.meta['height'] : '630' += opengraph 'twitter:card', 'summary_large_image' diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml index f1c6e6b9d..7a28f9738 100644 --- a/app/views/about/_registration.html.haml +++ b/app/views/about/_registration.html.haml @@ -1,26 +1,13 @@ = simple_form_for(new_user, url: user_registration_path) do |f| = f.simple_fields_for :account do |account_fields| .input-with-append - = account_fields.input :username, - autofocus: true, - placeholder: t('simple_form.labels.defaults.username'), - required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + = account_fields.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off' } .append = "@#{site_hostname}" - = f.input :email, - placeholder: t('simple_form.labels.defaults.email'), - required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, - placeholder: t('simple_form.labels.defaults.password'), - required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } - = f.input :password_confirmation, - placeholder: t('simple_form.labels.defaults.confirm_password'), - required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' } + = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' } + = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } + = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' } .actions = f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative' diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index 99d7d2972..6e4d0cdd1 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -3,16 +3,7 @@ - content_for :header_tags do = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - - %meta{ property: 'og:site_name', content: site_title }/ - %meta{ property: 'og:url', content: about_url }/ - %meta{ property: 'og:type', content: 'website' }/ - %meta{ property: 'og:title', content: site_hostname }/ - %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon_html')) }/ - %meta{ property: 'og:image', content: asset_pack_path('mastodon_small.jpg', protocol: :request) }/ - %meta{ property: 'og:image:width', content: '400' }/ - %meta{ property: 'og:image:height', content: '400' }/ - %meta{ property: 'twitter:card', content: 'summary' }/ + = render partial: 'og' .landing-page .header-wrapper.compact diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index 5962436fc..737dbbcef 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -4,16 +4,7 @@ - content_for :header_tags do %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous' - - %meta{ property: 'og:site_name', content: site_title }/ - %meta{ property: 'og:url', content: about_url }/ - %meta{ property: 'og:type', content: 'website' }/ - %meta{ property: 'og:title', content: site_hostname }/ - %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon_html')) }/ - %meta{ property: 'og:image', content: asset_pack_path('mastodon_small.jpg', protocol: :request) }/ - %meta{ property: 'og:image:width', content: '400' }/ - %meta{ property: 'og:image:height', content: '400' }/ - %meta{ property: 'twitter:card', content: 'summary' }/ + = render partial: 'og' .landing-page .header-wrapper diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index c16b7bf1f..dcc6661ba 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -43,15 +43,15 @@ .details-counters .counter{ class: active_nav_class(short_account_url(account)) } = link_to short_account_url(account), class: 'u-url u-uid' do - %span.counter-number= number_to_human account.statuses_count + %span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true %span.counter-label= t('accounts.posts') .counter{ class: active_nav_class(account_following_index_url(account)) } = link_to account_following_index_url(account) do - %span.counter-number= number_to_human account.following_count + %span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true %span.counter-label= t('accounts.following') .counter{ class: active_nav_class(account_followers_url(account)) } = link_to account_followers_url(account) do - %span.counter-number= number_to_human account.followers_count + %span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true %span.counter-label= t('accounts.followers') diff --git a/app/views/accounts/_og.html.haml b/app/views/accounts/_og.html.haml index 3ad39f391..1d16be590 100644 --- a/app/views/accounts/_og.html.haml +++ b/app/views/accounts/_og.html.haml @@ -1,8 +1,9 @@ -%meta{ property: 'og:url', content: url }/ -%meta{ property: 'og:site_name', content: site_title }/ -%meta{ property: 'og:title', content: [yield(:page_title).strip.presence, site_title].compact.join(' - ') }/ -%meta{ property: 'og:description', content: account.note }/ -%meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/ -%meta{ property: 'og:image:width', content: '120' }/ -%meta{ property: 'og:image:height', content: '120' }/ -%meta{ property: 'twitter:card', content: 'summary' }/ += opengraph 'og:url', url += opengraph 'og:site_name', site_title += opengraph 'og:title', [yield(:page_title).strip.presence, site_title].compact.join(' - ') += opengraph 'og:description', account.note += opengraph 'og:image', full_asset_url(account.avatar.url(:original)) += opengraph 'og:image:width', '120' += opengraph 'og:image:height', '120' += opengraph 'twitter:card', 'summary' += opengraph 'profile:username', account.local_username_and_domain diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index e0f9f869a..6c90b2c04 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -9,7 +9,7 @@ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/ - %meta{ property: 'og:type', content: 'profile' }/ + = opengraph 'og:type', 'profile' = render 'og', account: @account, url: short_account_url(@account, only_path: false) - if show_landing_strip? diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 89355281a..3775b6721 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -37,29 +37,6 @@ %th= t('admin.accounts.protocol') %td= @account.protocol.humanize - - if @account.ostatus? - %tr - %th= t('admin.accounts.feed_url') - %td= link_to @account.remote_url, @account.remote_url - %tr - %th= t('admin.accounts.push_subscription_expires') - %td - - if @account.subscribed? - %time.formatted{ datetime: @account.subscription_expires_at.iso8601, title: l(@account.subscription_expires_at) } - = l @account.subscription_expires_at - - else - = t('admin.accounts.not_subscribed') - %tr - %th= t('admin.accounts.salmon_url') - %td= link_to @account.salmon_url, @account.salmon_url - - elsif @account.activitypub? - %tr - %th= t('admin.accounts.inbox_url') - %td= link_to @account.inbox_url, @account.inbox_url - %tr - %th= t('admin.accounts.outbox_url') - %td= link_to @account.outbox_url, @account.outbox_url - %tr %th= t('admin.accounts.follows') %td= @account.following_count @@ -82,29 +59,73 @@ %th= t('.targeted_reports') %td= link_to pluralize(@account.targeted_reports.count, t('.report')), admin_reports_path(target_account_id: @account.id) -%div{ style: 'float: right' } - - if @account.local? - = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' - - if @account.user&.otp_required_for_login? - = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' - - else - - if @account.ostatus? +%div{ style: 'overflow: hidden' } + %div{ style: 'float: right' } + - if @account.local? + = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' + - if @account.user&.otp_required_for_login? + = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' + - else + = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' + + %div{ style: 'float: left' } + - if @account.silenced? + = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button' + - else + = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button' + + - if @account.local? + - unless @account.user_confirmed? + = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' + + - if @account.suspended? + = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button' + - else + = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' + +- unless @account.local? + %hr + %h3 OStatus + + .table-wrapper + %table.table + %tbody + %tr + %th= t('admin.accounts.feed_url') + %td= link_to @account.remote_url, @account.remote_url + %tr + %th= t('admin.accounts.push_subscription_expires') + %td + - if @account.subscribed? + %time.formatted{ datetime: @account.subscription_expires_at.iso8601, title: l(@account.subscription_expires_at) } + = l @account.subscription_expires_at + - else + = t('admin.accounts.not_subscribed') + %tr + %th= t('admin.accounts.salmon_url') + %td= link_to @account.salmon_url, @account.salmon_url + + %div{ style: 'overflow: hidden' } + %div{ style: 'float: right' } = link_to @account.subscribed? ? t('admin.accounts.resubscribe') : t('admin.accounts.subscribe'), subscribe_admin_account_path(@account.id), method: :post, class: 'button' - if @account.subscribed? = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' - = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' - -%div{ style: 'float: left' } - - if @account.silenced? - = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button' - - else - = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button' - - if @account.local? - - unless @account.user_confirmed? - = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' + %hr + %h3 ActivityPub - - if @account.suspended? - = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button' - - else - = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' + .table-wrapper + %table.table + %tbody + %tr + %th= t('admin.accounts.inbox_url') + %td= link_to @account.inbox_url, @account.inbox_url + %tr + %th= t('admin.accounts.outbox_url') + %td= link_to @account.outbox_url, @account.outbox_url + %tr + %th= t('admin.accounts.shared_inbox_url') + %td= link_to @account.shared_inbox_url, @account.shared_inbox_url + %tr + %th= t('admin.accounts.followers_url') + %td= link_to @account.followers_url, @account.followers_url diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml new file mode 100644 index 000000000..ff1aa9925 --- /dev/null +++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml @@ -0,0 +1,7 @@ +%tr + %td + = image_tag custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:" + %td + %samp= ":#{custom_emoji.shortcode}:" + %td + = table_link_to 'times', t('admin.custom_emojis.delete'), admin_custom_emoji_path(custom_emoji), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml new file mode 100644 index 000000000..d5f32e84b --- /dev/null +++ b/app/views/admin/custom_emojis/index.html.haml @@ -0,0 +1,14 @@ +- content_for :page_title do + = t('admin.custom_emojis.title') + +.table-wrapper + %table.table + %thead + %tr + %th= t('admin.custom_emojis.emoji') + %th= t('admin.custom_emojis.shortcode') + %th + %tbody + = render @custom_emojis + += link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button' diff --git a/app/views/admin/custom_emojis/new.html.haml b/app/views/admin/custom_emojis/new.html.haml new file mode 100644 index 000000000..672afe435 --- /dev/null +++ b/app/views/admin/custom_emojis/new.html.haml @@ -0,0 +1,12 @@ +- content_for :page_title do + = t('.title') + += simple_form_for @custom_emoji, url: admin_custom_emojis_path do |f| + = render 'shared/error_messages', object: @custom_emoji + + .fields-group + = f.input :shortcode, placeholder: t('admin.custom_emojis.shortcode'), hint: t('admin.custom_emojis.shortcode_hint') + = f.input :image, input_html: { accept: 'image/png' }, hint: t('admin.custom_emojis.image_hint') + + .actions + = f.button :button, t('admin.custom_emojis.upload'), type: :submit diff --git a/app/views/admin/instances/_instance.html.haml b/app/views/admin/instances/_instance.html.haml index 435cd8f64..6efbbbe60 100644 --- a/app/views/admin/instances/_instance.html.haml +++ b/app/views/admin/instances/_instance.html.haml @@ -1,6 +1,6 @@ %tr %td.domain - = instance.domain + = link_to instance.domain, admin_accounts_path(by_domain: instance.domain) %td.count = instance.accounts_count %td diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index edbd3b217..3314ce077 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -1,6 +1,16 @@ - content_for :page_title do = t('admin.instances.title') += form_tag admin_instances_url, method: 'GET', class: 'simple_form' do + .fields-group + - %i(domain_name).each do |key| + .input.string.optional + = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}") + + .actions + %button= t('admin.instances.search') + = link_to t('admin.instances.reset'), admin_instances_path, class: 'button negative' + .table-wrapper %table.table %thead diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 50d019ec4..468166035 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -11,6 +11,11 @@ %hr/ .fields-group + = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html') + + %hr/ + + .fields-group = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') .fields-group diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index d0529a20c..807020310 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -6,11 +6,11 @@ = f.simple_fields_for :account do |ff| .input-with-append - = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off' } .append = "@#{site_hostname}" - = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' } = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' } diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml index cb5e32f3e..2b07c923b 100644 --- a/app/views/auth/sessions/two_factor.html.haml +++ b/app/views/auth/sessions/two_factor.html.haml @@ -2,9 +2,7 @@ = t('auth.login') = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), - input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true, - hint: t('simple_form.hints.sessions.otp') + = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true, hint: t('simple_form.hints.sessions.otp') .actions = f.button :button, t('auth.login'), type: :submit diff --git a/app/views/oauth/authorizations/show.html.haml b/app/views/oauth/authorizations/show.html.haml index b56667f35..ad5236007 100644 --- a/app/views/oauth/authorizations/show.html.haml +++ b/app/views/oauth/authorizations/show.html.haml @@ -1,3 +1,4 @@ .form-container .flash-message - %code= params[:code] + %p= t('doorkeeper.authorizations.show.title') + %input{ type: 'text', class: 'oauth-code', readonly: true, value: params[:code], onClick: 'select()' } diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 3fa540bba..551a7ca49 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -8,8 +8,8 @@ = f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name'), hint: t('simple_form.hints.defaults.display_name', count: 30 - @account.display_name.size).html_safe = f.input :note, placeholder: t('simple_form.labels.defaults.note'), hint: t('simple_form.hints.defaults.note', count: 500 - @account.note.size).html_safe - .card.compact{ style: "background-image: url(#{@account.header.url(:original)})" } - .avatar= image_tag @account.avatar.url(:original) + .card.compact{ style: "background-image: url(#{@account.header.url(:original)})", data: { original_src: @account.header.url(:original) } } + .avatar= image_tag @account.avatar.url(:original), data: { original_src: @account.avatar.url(:original) } .fields-group = f.input :avatar, wrapper: :with_label, input_html: { accept: AccountAvatar::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.avatar') diff --git a/app/views/stream_entries/_og_description.html.haml b/app/views/stream_entries/_og_description.html.haml index 5762aca04..d2fa99e63 100644 --- a/app/views/stream_entries/_og_description.html.haml +++ b/app/views/stream_entries/_og_description.html.haml @@ -1,4 +1,4 @@ - if activity.is_a?(Status) && activity.spoiler_text? - %meta{ property: 'og:description', content: activity.spoiler_text }/ + = opengraph 'og:description', activity.spoiler_text - else - %meta{ property: 'og:description', content: activity.content }/ + = opengraph 'og:description', activity.content diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml index f725209d8..b5058583b 100644 --- a/app/views/stream_entries/_og_image.html.haml +++ b/app/views/stream_entries/_og_image.html.haml @@ -1,6 +1,23 @@ - if activity.is_a?(Status) && activity.non_sensitive_with_media? - %meta{ property: 'og:image', content: full_asset_url(activity.media_attachments.first.file.url(:small)) }/ + - activity.media_attachments.each do |media| + - if media.image? + = opengraph 'og:image', full_asset_url(media.file.url(:original)) + = opengraph 'og:image:type', media.file_content_type + - unless media.file.meta.nil? + = opengraph 'og:image:width', media.file.meta['original']['width'] + = opengraph 'og:image:height', media.file.meta['original']['height'] + - elsif media.video? + = opengraph 'og:image', full_asset_url(media.file.url(:small)) + = opengraph 'og:image:type', 'image/png' + - unless media.file.meta.nil? + = opengraph 'og:image:width', media.file.meta['small']['width'] + = opengraph 'og:image:height', media.file.meta['small']['height'] + = opengraph 'og:video', full_asset_url(media.file.url(:original)) + = opengraph 'og:video:type', media.file_content_type + - unless media.file.meta.nil? + = opengraph 'og:video:width', media.file.meta['small']['width'] + = opengraph 'og:video:height', media.file.meta['small']['height'] - else - %meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/ - %meta{ property: 'og:image:width', content: '120' }/ - %meta{ property: 'og:image:height', content: '120' }/ + = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) + = opengraph 'og:image:width', '120' + = opengraph 'og:image:height','120' diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml index 5ef72f804..1bb8a32b2 100644 --- a/app/views/stream_entries/show.html.haml +++ b/app/views/stream_entries/show.html.haml @@ -6,15 +6,15 @@ %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/ %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@stream_entry.activity) }/ - %meta{ property: 'og:site_name', content: site_title }/ - %meta{ property: 'og:type', content: 'article' }/ - %meta{ property: 'og:title', content: "#{@account.username} on #{site_hostname}" }/ - %meta{ property: 'og:url', content: account_stream_entry_url(@account, @stream_entry) }/ + = opengraph 'og:site_name', site_title + = opengraph 'og:type', 'article' + = opengraph 'og:title', "#{@account.username} on #{site_hostname}" + = opengraph 'og:url', account_stream_entry_url(@account, @stream_entry) = render 'stream_entries/og_description', activity: @stream_entry.activity = render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account - %meta{ property: 'twitter:card', content: 'summary' }/ + = opengraph 'twitter:card', 'summary_large_image' - if show_landing_strip? = render partial: 'shared/landing_strip', locals: { account: @stream_entry.account } diff --git a/app/views/user_mailer/confirmation_instructions.es.html.erb b/app/views/user_mailer/confirmation_instructions.es.html.erb new file mode 100644 index 000000000..1d46a12c0 --- /dev/null +++ b/app/views/user_mailer/confirmation_instructions.es.html.erb @@ -0,0 +1,12 @@ +<p>¡Bienvenido, <%= @resource.email %>!</p> + +<p>Acabas de crear una cuenta en <%= @instance %>.</p> + +<p>Para confirmar tu registro, por favor ingresa al siguiente enlace:<br> +<%= link_to 'Confirmar mi cuenta', confirmation_url(@resource, confirmation_token: @token) %> + +<p>También revisa nuestros <%= link_to 'términos y condiciones', terms_url %>.</p> + +<p>Sinceramente,<p> + +<p>El equipo de <%= @instance %></p> \ No newline at end of file diff --git a/app/views/user_mailer/confirmation_instructions.es.text.erb b/app/views/user_mailer/confirmation_instructions.es.text.erb new file mode 100644 index 000000000..e9d83b3f8 --- /dev/null +++ b/app/views/user_mailer/confirmation_instructions.es.text.erb @@ -0,0 +1,12 @@ +¡Bienvenido, <%= @resource.email %>! + +Acabas de crear una cuenta en <%= @instance %>. + +Para confirmar tu registro, por favor ingresa al siguiente enlace: +<%= confirmation_url(@resource, confirmation_token: @token) %> + +Por favor, también revisa nuestros términos y condiciones <%= terms_url %> + +Sinceramente, + +El equipo de <%= @instance %> \ No newline at end of file diff --git a/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb b/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb new file mode 100644 index 000000000..80edcfda7 --- /dev/null +++ b/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb @@ -0,0 +1,12 @@ +<p>Boas vindas, <%= @resource.email %>!</p> + +<p>Você acabou de criar uma conta no <%= @instance %>.</p> + +<p>Para confirmar o seu cadastro, por favor clique no link a seguir: <br> +<%= link_to 'Confirmar cadastro', confirmation_url(@resource, confirmation_token: @token) %> + +<p>Por favor, leia também os nossos <%= link_to 'termos de serviços', terms_url %>.</p> + +<p>Atenciosamente,<p> + +<p>A equipe do <%= @instance %></p> diff --git a/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb b/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb new file mode 100644 index 000000000..95efb3436 --- /dev/null +++ b/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb @@ -0,0 +1,12 @@ +Boas vindas, <%= @resource.email %>! + +Você acabou de criar uma conta no <%= @instance %>. + +Para confirmar o seu cadastro, por favor clique no link a seguir: +<%= confirmation_url(@resource, confirmation_token: @token) %> + +Por favor, leia também os nossos termos e condições de uso <%= terms_url %> + +Atenciosamente, + +A equipe do <%= @instance %> diff --git a/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb b/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb index 575b2ff9e..de2f8b6e0 100644 --- a/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb +++ b/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb @@ -3,7 +3,7 @@ <p>你刚刚在 <%= @instance %> 创建了帐号。</p> <p>点击下面的链接来完成注册啦 : <br> -<%= link_to '确认账户', confirmation_url(@resource, confirmation_token: @token) %> +<%= link_to '确认帐户', confirmation_url(@resource, confirmation_token: @token) %> <p>别忘了看看 <%= link_to '使用条款', terms_url %>。</p> diff --git a/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb b/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb index ce237a32d..d7d4b4b23 100644 --- a/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb +++ b/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb @@ -3,7 +3,7 @@ 你刚刚在 <%= @instance %> 创建了帐号。 点击下面的链接来完成注册啦 : <br> -<%= link_to '确认账户', confirmation_url(@resource, confirmation_token: @token) %> +<%= link_to '确认帐户', confirmation_url(@resource, confirmation_token: @token) %> 别忘了看看 <%= link_to 'terms and conditions', terms_url %>。 diff --git a/app/views/user_mailer/password_change.es.html.erb b/app/views/user_mailer/password_change.es.html.erb new file mode 100644 index 000000000..0a9eb4c4c --- /dev/null +++ b/app/views/user_mailer/password_change.es.html.erb @@ -0,0 +1,3 @@ +<p>¡Hola, <%= @resource.email %>!</p> + +<p>Te contactamos para notificarte que tu contraseña en <%= @instance %> ha sido modificada.</p> \ No newline at end of file diff --git a/app/views/user_mailer/password_change.es.text.erb b/app/views/user_mailer/password_change.es.text.erb new file mode 100644 index 000000000..192faf9ad --- /dev/null +++ b/app/views/user_mailer/password_change.es.text.erb @@ -0,0 +1,3 @@ +¡Hola, <%= @resource.email %>! + +Te contactamos para notificarte que tu contraseña en <%= @instance %> ha sido modificada. \ No newline at end of file diff --git a/app/views/user_mailer/password_change.pt-BR.html.erb b/app/views/user_mailer/password_change.pt-BR.html.erb new file mode 100644 index 000000000..5f707ba09 --- /dev/null +++ b/app/views/user_mailer/password_change.pt-BR.html.erb @@ -0,0 +1,3 @@ +<p>Olá, <%= @resource.email %>!</p> + +<p>Estamos te contatando para te notificar que a senha senha no <%= @instance %> foi modificada.</p> diff --git a/app/views/user_mailer/password_change.pt-BR.text.erb b/app/views/user_mailer/password_change.pt-BR.text.erb new file mode 100644 index 000000000..d8b76648c --- /dev/null +++ b/app/views/user_mailer/password_change.pt-BR.text.erb @@ -0,0 +1,3 @@ +Olá, <%= @resource.email %>! + +Estamos te contatando para te notificar que a senha senha no <%= @instance %> foi modificada. diff --git a/app/views/user_mailer/reset_password_instructions.es.html.erb b/app/views/user_mailer/reset_password_instructions.es.html.erb new file mode 100644 index 000000000..4eeb6601d --- /dev/null +++ b/app/views/user_mailer/reset_password_instructions.es.html.erb @@ -0,0 +1,8 @@ +<p>¡Hola, <%= @resource.email %>!</p> + +<p>Alguien pidió un enlace para cambiar tu contraseña en <%= @instance %>. Puedes hacer esto con el siguiente enlace.</p> + +<p><%= link_to 'Cambiar mi contraseña', edit_password_url(@resource, reset_password_token: @token) %></p> + +<p>Si no fuiste tú, por favor ignora este mensaje.</p> +<p>Tu contraseña no cambiará hasta que ingreses al enlace y crees una nueva.</p> diff --git a/app/views/user_mailer/reset_password_instructions.es.text.erb b/app/views/user_mailer/reset_password_instructions.es.text.erb new file mode 100644 index 000000000..8abafcc99 --- /dev/null +++ b/app/views/user_mailer/reset_password_instructions.es.text.erb @@ -0,0 +1,8 @@ +¡Hola, <%= @resource.email %>! + +Alguien pidió un enlace para cambiar tu contraseña en <%= @instance %>. Puedes hacer esto con el siguiente enlace. + +<%= edit_password_url(@resource, reset_password_token: @token) %> + +Si no fuiste tú, por favor ignora este mensaje. +Tu contraseña no cambiará hasta que ingreses al enlace y crees una nueva. diff --git a/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb b/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb new file mode 100644 index 000000000..940438b7c --- /dev/null +++ b/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb @@ -0,0 +1,8 @@ +<p>Olá, <%= @resource.email %>!</p> + +<p>Alguém solicitou um link para mudar a sua senha no <%= @instance %>. Você pode fazer isso através do link abaixo:</p> + +<p><%= link_to 'Mudar a minha senha', edit_password_url(@resource, reset_password_token: @token) %></p> + +<p>Se você não solicitou isso, por favor ignore este e-mail.</p> +<p>A senha senha não será modificada até que você acesse o link acima e crie uma nova.</p> diff --git a/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb b/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb new file mode 100644 index 000000000..f574fe08f --- /dev/null +++ b/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb @@ -0,0 +1,8 @@ +Olá, <%= @resource.email %>! + +Alguém solicitou um link para mudar a sua senha no <%= @instance %>. Você pode fazer isso através do link abaixo: + +<%= edit_password_url(@resource, reset_password_token: @token) %> + +Se você não solicitou isso, por favor ignore este e-mail. +A senha senha não será modificada até que você acesse o link acima e crie uma nova. diff --git a/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb b/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb index 245382b2c..51e3073f1 100644 --- a/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb +++ b/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb @@ -1,6 +1,6 @@ <p><%= @resource.email %> ,嗨呀!!</p> -<p>有人(但愿是你)请求更改你Mastodon账户的密码。如果是你的话,请点击下面的链接:</p> +<p>有人(但愿是你)请求更改你Mastodon帐户的密码。如果是你的话,请点击下面的链接:</p> <p><%= link_to '更改密码', edit_password_url(@resource, reset_password_token: @token) %></p> diff --git a/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb b/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb index 574a0bb2e..7df590f78 100644 --- a/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb +++ b/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb @@ -1,6 +1,6 @@ <%= @resource.email %> ,嗨呀!! -有人(但愿是你)请求更改你Mastodon账户的密码。如果是你的话,请点击下面的链接: +有人(但愿是你)请求更改你Mastodon帐户的密码。如果是你的话,请点击下面的链接: <%= link_to '更改密码', edit_password_url(@resource, reset_password_token: @token) %> diff --git a/app/workers/activitypub/reply_distribution_worker.rb b/app/workers/activitypub/reply_distribution_worker.rb index f9127340f..fe99fc05f 100644 --- a/app/workers/activitypub/reply_distribution_worker.rb +++ b/app/workers/activitypub/reply_distribution_worker.rb @@ -7,9 +7,9 @@ class ActivityPub::ReplyDistributionWorker def perform(status_id) @status = Status.find(status_id) - @account = @status.thread.account + @account = @status.thread&.account - return if skip_distribution? + return if @account.nil? || skip_distribution? ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| [signed_payload, @status.account_id, inbox_url] diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb index 130c967e0..7560c2671 100644 --- a/app/workers/pubsubhubbub/subscribe_worker.rb +++ b/app/workers/pubsubhubbub/subscribe_worker.rb @@ -3,7 +3,7 @@ class Pubsubhubbub::SubscribeWorker include Sidekiq::Worker - sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false, unique_retry: true + sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false sidekiq_retry_in do |count| case count diff --git a/app/workers/refollow_worker.rb b/app/workers/refollow_worker.rb new file mode 100644 index 000000000..66bcd27c3 --- /dev/null +++ b/app/workers/refollow_worker.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class RefollowWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull', retry: false + + def perform(target_account_id) + target_account = Account.find(target_account_id) + return unless target_account.protocol == :activitypub + + target_account.followers.where(domain: nil).find_each do |follower| + # Locally unfollow remote account + follower.unfollow!(target_account) + + # Schedule re-follow + begin + FollowService.new.call(follower, target_account) + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError + next + end + end + end +end diff --git a/app/workers/scheduler/ip_cleanup_scheduler.rb b/app/workers/scheduler/ip_cleanup_scheduler.rb new file mode 100644 index 000000000..9f1593c91 --- /dev/null +++ b/app/workers/scheduler/ip_cleanup_scheduler.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require 'sidekiq-scheduler' + +class Scheduler::IpCleanupScheduler + include Sidekiq::Worker + + def perform + time_ago = 5.years.ago + SessionActivation.where('updated_at < ?', time_ago).destroy_all + User.where('last_sign_in_at < ?', time_ago).update_all(last_sign_in_ip: nil) + end +end |