mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-12 23:38:20 +00:00
Compare commits
62 Commits
77a316e475
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88c0f52e99 | ||
|
|
a56b739c68 | ||
|
|
183a42a5ee | ||
|
|
303a5478af | ||
|
|
dfbf908870 | ||
|
|
aa067370d8 | ||
|
|
5e0db46b2a | ||
|
|
c06eb371e6 | ||
|
|
53617cef5a | ||
|
|
d730f6b0c5 | ||
|
|
addeb28292 | ||
|
|
5e3387539e | ||
|
|
4323963053 | ||
|
|
5651900b89 | ||
|
|
d1b996b7e3 | ||
|
|
fed26a41fa | ||
|
|
37d309bcaf | ||
|
|
d25f672c50 | ||
|
|
15c9088761 | ||
|
|
da1505a495 | ||
|
|
d1f690f50c | ||
|
|
8b418b84d0 | ||
|
|
f817300d8d | ||
|
|
35a89a0173 | ||
|
|
b5721dbd4a | ||
|
|
38f623eee7 | ||
|
|
17ba99e5de | ||
|
|
da2b75bdcd | ||
|
|
adf8a3601d | ||
|
|
d6f2a3ac8d | ||
|
|
c42b9f6996 | ||
|
|
76184c998c | ||
|
|
8137ce87ce | ||
|
|
37426288d9 | ||
|
|
801fee7593 | ||
|
|
6838497fe8 | ||
|
|
7b8a5d42f1 | ||
|
|
cd71fdcdff | ||
|
|
91500a7f53 | ||
|
|
5422e43e31 | ||
|
|
5a66331003 | ||
|
|
09e3955145 | ||
|
|
e554e5723d | ||
|
|
315f5e5a31 | ||
|
|
9d81561bb2 | ||
|
|
ac71771d98 | ||
|
|
697569e5f9 | ||
|
|
4cdcdaa7d9 | ||
|
|
9702cbb41c | ||
|
|
ea768c17db | ||
|
|
5347cabf3e | ||
|
|
eef40ba96b | ||
|
|
9063c3b660 | ||
|
|
e147947eb8 | ||
|
|
8c52889c86 | ||
|
|
05e45beb34 | ||
|
|
607449336d | ||
|
|
85bf5be604 | ||
|
|
cf23f0414f | ||
|
|
55becaa1b5 | ||
|
|
8625721805 | ||
|
|
7fe3e80758 |
42
.github/workflows/build-releases.yml
vendored
42
.github/workflows/build-releases.yml
vendored
@@ -9,7 +9,44 @@ permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
check-latest-stable:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
latest: ${{ steps.check.outputs.is_latest_stable }}
|
||||
steps:
|
||||
# Repository needs to be cloned to list branches
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check latest stable
|
||||
shell: bash
|
||||
id: check
|
||||
run: |
|
||||
ref="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
if [[ "$ref" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?$ ]]; then
|
||||
current="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
|
||||
else
|
||||
echo "tag $ref is not semver"
|
||||
echo "is_latest_stable=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
latest=$(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/stable-*.*" \
|
||||
| sed -E 's#^origin/stable-##' \
|
||||
| sort -Vr \
|
||||
| head -n1)
|
||||
|
||||
if [[ "$current" == "$latest" ]]; then
|
||||
echo "is_latest_stable=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_latest_stable=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build-image:
|
||||
needs: check-latest-stable
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
file_to_build: Dockerfile
|
||||
@@ -20,13 +57,14 @@ jobs:
|
||||
# Only tag with latest when ran against the latest stable branch
|
||||
# This needs to be updated after each minor version release
|
||||
flavor: |
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }}
|
||||
latest=${{ needs.check-latest-stable.outputs.latest }}
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
secrets: inherit
|
||||
|
||||
build-image-streaming:
|
||||
needs: check-latest-stable
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
file_to_build: streaming/Dockerfile
|
||||
@@ -37,7 +75,7 @@ jobs:
|
||||
# Only tag with latest when ran against the latest stable branch
|
||||
# This needs to be updated after each minor version release
|
||||
flavor: |
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }}
|
||||
latest=${{ needs.check-latest-stable.outputs.latest }}
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/lib/index.js b/lib/index.js
|
||||
index 16ed6be8be8f555cc99096c2ff60954b42dc313d..d009c069770d066ad0db7ad02de1ea473a29334e 100644
|
||||
--- a/lib/index.js
|
||||
+++ b/lib/index.js
|
||||
@@ -99,7 +99,7 @@ function lodash(_ref) {
|
||||
|
||||
var node = _ref3;
|
||||
|
||||
- if ((0, _types.isModuleDeclaration)(node)) {
|
||||
+ if ((0, _types.isImportDeclaration)(node) || (0, _types.isExportDeclaration)(node)) {
|
||||
isModule = true;
|
||||
break;
|
||||
}
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -2,6 +2,26 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.5.3] - 2025-12-08
|
||||
|
||||
### Security
|
||||
|
||||
- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix “Delete and Redraft” on a non-quote being treated as a quote post in some cases (#37140 by @ClearlyClaire)
|
||||
- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima)
|
||||
- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire)
|
||||
- Fix creation of duplicate conversations (#37108 by @oneiros)
|
||||
- Fix extraneous `noreferrer` in external links (#37107 by @ChaosExAnima)
|
||||
- Fix edge case error handling in some database migrations (#37079 by @ClearlyClaire)
|
||||
- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire)
|
||||
- Fix post navigation in single-column mode when Advanced UI is enabled (#37044 by @diondiondion)
|
||||
- Fix `tootctl status remove` removing quoted posts and remote quotes of local posts (#37009 by @ClearlyClaire)
|
||||
- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire)
|
||||
- Fix compose autosuggest always lowercasing input token (#36995 by @ClearlyClaire)
|
||||
|
||||
## [4.5.2] - 2025-11-20
|
||||
|
||||
### Changed
|
||||
|
||||
17
Gemfile.lock
17
Gemfile.lock
@@ -53,7 +53,7 @@ GEM
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_model_serializers (0.10.15)
|
||||
active_model_serializers (0.10.16)
|
||||
actionpack (>= 4.1)
|
||||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
@@ -304,8 +304,8 @@ GEM
|
||||
highline (3.1.2)
|
||||
reline
|
||||
hiredis (0.6.3)
|
||||
hiredis-client (0.26.1)
|
||||
redis-client (= 0.26.1)
|
||||
hiredis-client (0.26.2)
|
||||
redis-client (= 0.26.2)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http (5.3.1)
|
||||
@@ -469,7 +469,7 @@ GEM
|
||||
nokogiri (1.18.10)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.12)
|
||||
oj (3.16.13)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (2.1.4)
|
||||
@@ -481,7 +481,7 @@ GEM
|
||||
addressable (~> 2.8)
|
||||
nokogiri (~> 1.12)
|
||||
omniauth (~> 2.1)
|
||||
omniauth-rails_csrf_protection (2.0.0)
|
||||
omniauth-rails_csrf_protection (2.0.1)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-saml (2.2.4)
|
||||
@@ -703,7 +703,7 @@ GEM
|
||||
reline
|
||||
redcarpet (3.6.1)
|
||||
redis (4.8.1)
|
||||
redis-client (0.26.1)
|
||||
redis-client (0.26.2)
|
||||
connection_pool
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
@@ -839,7 +839,8 @@ GEM
|
||||
stackprof (0.2.27)
|
||||
starry (0.2.0)
|
||||
base64
|
||||
stoplight (5.6.0)
|
||||
stoplight (5.7.0)
|
||||
concurrent-ruby
|
||||
zeitwerk
|
||||
stringio (3.1.8)
|
||||
strong_migrations (2.5.1)
|
||||
@@ -855,7 +856,7 @@ GEM
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
terrapin (1.1.1)
|
||||
climate_control
|
||||
test-prof (1.4.4)
|
||||
test-prof (1.5.0)
|
||||
thor (1.4.0)
|
||||
tilt (2.6.1)
|
||||
timeout (0.4.3)
|
||||
|
||||
@@ -22,7 +22,7 @@ class ActivityPub::LikesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
|
||||
return not_found unless @quote.status.present? && @quote.quoted_status.present?
|
||||
|
||||
authorize @quote.quoted_status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class ActivityPub::SharesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
|
||||
def set_poll
|
||||
@poll = Poll.find(params[:poll_id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
|
||||
def set_poll
|
||||
@poll = Poll.find(params[:id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Statuses::BaseController < Api::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
|
||||
bookmark&.destroy!
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseControlle
|
||||
|
||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } })
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: relationships
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,7 +36,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
||||
|
||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } })
|
||||
render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
||||
def set_reblog
|
||||
@reblog = Status.find(params[:status_id])
|
||||
authorize @reblog, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -3,20 +3,34 @@
|
||||
class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
DEFAULT_COLLECTIONS_LIMIT = 40
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||
render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422
|
||||
end
|
||||
|
||||
before_action :check_feature_enabled
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index, :show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create, :update, :destroy]
|
||||
|
||||
before_action :require_user!, only: [:create, :update, :destroy]
|
||||
|
||||
before_action :set_account, only: [:index]
|
||||
before_action :set_collections, only: [:index]
|
||||
before_action :set_collection, only: [:show, :update, :destroy]
|
||||
|
||||
after_action :insert_pagination_headers, only: [:index]
|
||||
|
||||
after_action :verify_authorized
|
||||
|
||||
def index
|
||||
cache_if_unauthenticated!
|
||||
authorize Collection, :index?
|
||||
|
||||
render json: @collections, each_serializer: REST::BaseCollectionSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
cache_if_unauthenticated!
|
||||
authorize @collection, :show?
|
||||
@@ -50,6 +64,18 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_collections
|
||||
@collections = @account.collections
|
||||
.with_tag
|
||||
.order(created_at: :desc)
|
||||
.offset(offset_param)
|
||||
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
|
||||
end
|
||||
|
||||
def set_collection
|
||||
@collection = Collection.find(params[:id])
|
||||
end
|
||||
@@ -65,4 +91,24 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||
def check_feature_enabled
|
||||
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
|
||||
end
|
||||
|
||||
def next_path
|
||||
return unless records_continue?
|
||||
|
||||
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
|
||||
end
|
||||
|
||||
def prev_path
|
||||
return if offset_param.zero?
|
||||
|
||||
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
((offset_param * limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections.size) < @account.collections.size
|
||||
end
|
||||
|
||||
def offset_param
|
||||
params[:offset].to_i
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
|
||||
def set_resource
|
||||
@resource = located_resource
|
||||
authorize(@resource, :show?) if @resource.is_a?(Status)
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class MediaController < ApplicationController
|
||||
|
||||
def verify_permitted_status!
|
||||
authorize @media_attachment.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class StatusesController < ApplicationController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ module ApplicationHelper
|
||||
|
||||
def html_classes
|
||||
output = []
|
||||
output << content_for(:html_classes)
|
||||
output << 'system-font' if current_account&.user&.setting_system_font_ui
|
||||
output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui
|
||||
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DateOfBirthInput < SimpleForm::Inputs::Base
|
||||
OPTIONS = [
|
||||
{ autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze,
|
||||
{ autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze,
|
||||
{ autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze,
|
||||
].freeze
|
||||
OPTIONS = {
|
||||
day: { autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' },
|
||||
month: { autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' },
|
||||
year: { autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' },
|
||||
}.freeze
|
||||
|
||||
def input(wrapper_options = nil)
|
||||
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
||||
merged_input_options[:inputmode] = 'numeric'
|
||||
|
||||
values = (object.public_send(attribute_name) || '').to_s.split('-')
|
||||
|
||||
safe_join(2.downto(0).map do |index|
|
||||
options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index]
|
||||
@builder.text_field("#{attribute_name}(#{index + 1}i)", options)
|
||||
end)
|
||||
safe_join(
|
||||
ordered_options.map do |option|
|
||||
options = merged_input_options
|
||||
.merge(OPTIONS[option])
|
||||
.merge(
|
||||
id: generate_id(option),
|
||||
'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{param_for(option)}"),
|
||||
value: values[option]
|
||||
)
|
||||
@builder.text_field("#{attribute_name}(#{param_for(option)})", options)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def label_target
|
||||
"#{attribute_name}_3i"
|
||||
"#{attribute_name}_#{param_for(ordered_options.first)}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_id(index)
|
||||
"#{object_name}_#{attribute_name}_#{index + 1}i"
|
||||
def ordered_options
|
||||
I18n.t('date.order').map(&:to_sym)
|
||||
end
|
||||
|
||||
def generate_id(option)
|
||||
"#{object_name}_#{attribute_name}_#{param_for(option)}"
|
||||
end
|
||||
|
||||
def param_for(option)
|
||||
"#{ActionView::Helpers::DateTimeSelector::POSITION[option]}i"
|
||||
end
|
||||
|
||||
def values
|
||||
Date._parse((object.public_send(attribute_name) || '').to_s).transform_keys(mon: :month, mday: :day)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,13 +2,11 @@ import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
|
||||
import {
|
||||
importFetchedAccounts,
|
||||
importFetchedStatuses,
|
||||
} from '@/mastodon/actions/importer';
|
||||
import { importFetchedStatuses } from '@/mastodon/actions/importer';
|
||||
import { hydrateStore } from '@/mastodon/actions/store';
|
||||
import type { ApiAnnualReportResponse } from '@/mastodon/api/annual_report';
|
||||
import { Router } from '@/mastodon/components/router';
|
||||
import { WrapstodonShare } from '@/mastodon/features/annual_report/share';
|
||||
import { WrapstodonSharedPage } from '@/mastodon/features/annual_report/shared_page';
|
||||
import { IntlProvider, loadLocale } from '@/mastodon/locales';
|
||||
import { loadPolyfills } from '@/mastodon/polyfills';
|
||||
import ready from '@/mastodon/ready';
|
||||
@@ -33,7 +31,14 @@ function loaded() {
|
||||
if (!report) {
|
||||
throw new Error('Initial state report not found');
|
||||
}
|
||||
store.dispatch(importFetchedAccounts(initialState.accounts));
|
||||
|
||||
// Set up store
|
||||
store.dispatch(
|
||||
hydrateStore({
|
||||
meta: { locale: document.documentElement.lang },
|
||||
accounts: initialState.accounts,
|
||||
}),
|
||||
);
|
||||
store.dispatch(importFetchedStatuses(initialState.statuses));
|
||||
|
||||
store.dispatch(setReport(report));
|
||||
@@ -43,7 +48,7 @@ function loaded() {
|
||||
<IntlProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<Router>
|
||||
<WrapstodonShare />
|
||||
<WrapstodonSharedPage />
|
||||
</Router>
|
||||
</ReduxProvider>
|
||||
</IntlProvider>,
|
||||
|
||||
@@ -37,7 +37,9 @@ export function hydrateStore(rawState) {
|
||||
|
||||
dispatch(hydrateCompose());
|
||||
dispatch(hydrateSearch());
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
if (rawState.accounts) {
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
}
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import { reinsertAnnualReport, TIMELINE_WRAPSTODON } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||
import api, { getLinks } from 'flavours/glitch/api';
|
||||
import { compareId } from 'flavours/glitch/compare_id';
|
||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
||||
@@ -7,7 +8,7 @@ import { toServerSideType } from 'flavours/glitch/utils/filters';
|
||||
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import {timelineDelete} from './timelines_typed';
|
||||
import { timelineDelete } from './timelines_typed';
|
||||
|
||||
export { disconnectTimeline } from './timelines_typed';
|
||||
|
||||
@@ -25,9 +26,16 @@ export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
|
||||
export const TIMELINE_INSERT = 'TIMELINE_INSERT';
|
||||
|
||||
// When adding new special markers here, make sure to update TIMELINE_NON_STATUS_MARKERS in actions/timelines_typed.js
|
||||
export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions';
|
||||
export const TIMELINE_GAP = null;
|
||||
|
||||
export const TIMELINE_NON_STATUS_MARKERS = [
|
||||
TIMELINE_GAP,
|
||||
TIMELINE_SUGGESTIONS,
|
||||
TIMELINE_WRAPSTODON,
|
||||
];
|
||||
|
||||
export const loadPending = timeline => ({
|
||||
type: TIMELINE_LOAD_PENDING,
|
||||
timeline,
|
||||
@@ -135,6 +143,7 @@ export function expandTimeline(timelineId, path, params = {}) {
|
||||
|
||||
if (timelineId === 'home') {
|
||||
dispatch(submitMarkers());
|
||||
dispatch(reinsertAnnualReport())
|
||||
}
|
||||
} catch(error) {
|
||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||
|
||||
@@ -2,6 +2,12 @@ import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
||||
|
||||
import { TIMELINE_NON_STATUS_MARKERS } from './timelines';
|
||||
|
||||
export function isNonStatusId(value: unknown) {
|
||||
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
||||
}
|
||||
|
||||
export const disconnectTimeline = createAction(
|
||||
'timeline/disconnect',
|
||||
({ timeline }: { timeline: string }) => ({
|
||||
|
||||
@@ -115,6 +115,7 @@ class Status extends ImmutablePureComponent {
|
||||
muted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
showActions: PropTypes.bool,
|
||||
prepend: PropTypes.string,
|
||||
withDismiss: PropTypes.bool,
|
||||
isQuotedPost: PropTypes.bool,
|
||||
@@ -465,7 +466,8 @@ class Status extends ImmutablePureComponent {
|
||||
onOpenMedia,
|
||||
notification,
|
||||
history,
|
||||
isQuotedPost,
|
||||
showActions = true,
|
||||
isQuotedPost = false,
|
||||
...other
|
||||
} = this.props;
|
||||
let attachments = null;
|
||||
@@ -763,7 +765,7 @@ class Status extends ImmutablePureComponent {
|
||||
{/* This is a glitch-soc addition to have a placeholder */}
|
||||
{!expanded && <MentionsPlaceholder status={status} />}
|
||||
|
||||
{!isQuotedPost &&
|
||||
{(showActions && !isQuotedPost) &&
|
||||
<StatusActionBar
|
||||
status={status}
|
||||
account={status.get('account')}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
export const InterceptStatusClicks: React.FC<{
|
||||
onPreventedClick: (
|
||||
clickedArea: 'account' | 'post',
|
||||
event: React.MouseEvent,
|
||||
) => void;
|
||||
children: React.ReactNode;
|
||||
}> = ({ onPreventedClick, children }) => {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const clickTarget = e.target as Element;
|
||||
const allowedElementsSelector =
|
||||
'.video-player, .audio-player, .media-gallery, .content-warning';
|
||||
const allowedElements = wrapperRef.current?.querySelectorAll(
|
||||
allowedElementsSelector,
|
||||
);
|
||||
const isTargetClickAllowed =
|
||||
allowedElements &&
|
||||
Array.from(allowedElements).some((element) => {
|
||||
return clickTarget === element || element.contains(clickTarget);
|
||||
});
|
||||
|
||||
if (!isTargetClickAllowed) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const wasAccountAreaClicked = !!clickTarget.closest(
|
||||
'a.status__display-name',
|
||||
);
|
||||
|
||||
onPreventedClick(wasAccountAreaClicked ? 'account' : 'post', e);
|
||||
}
|
||||
},
|
||||
[onPreventedClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} onClickCapture={handleClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,13 +2,11 @@ import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
|
||||
import {
|
||||
importFetchedAccounts,
|
||||
importFetchedStatuses,
|
||||
} from '@/flavours/glitch/actions/importer';
|
||||
import { importFetchedStatuses } from '@/flavours/glitch/actions/importer';
|
||||
import { hydrateStore } from '@/flavours/glitch/actions/store';
|
||||
import type { ApiAnnualReportResponse } from '@/flavours/glitch/api/annual_report';
|
||||
import { Router } from '@/flavours/glitch/components/router';
|
||||
import { WrapstodonShare } from '@/flavours/glitch/features/annual_report/share';
|
||||
import { WrapstodonSharedPage } from '@/flavours/glitch/features/annual_report/shared_page';
|
||||
import { IntlProvider, loadLocale } from '@/flavours/glitch/locales';
|
||||
import { loadPolyfills } from '@/flavours/glitch/polyfills';
|
||||
import ready from '@/flavours/glitch/ready';
|
||||
@@ -33,7 +31,14 @@ function loaded() {
|
||||
if (!report) {
|
||||
throw new Error('Initial state report not found');
|
||||
}
|
||||
store.dispatch(importFetchedAccounts(initialState.accounts));
|
||||
|
||||
// Set up store
|
||||
store.dispatch(
|
||||
hydrateStore({
|
||||
meta: { locale: document.documentElement.lang },
|
||||
accounts: initialState.accounts,
|
||||
}),
|
||||
);
|
||||
store.dispatch(importFetchedStatuses(initialState.statuses));
|
||||
|
||||
store.dispatch(setReport(report));
|
||||
@@ -43,7 +48,7 @@ function loaded() {
|
||||
<IntlProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<Router>
|
||||
<WrapstodonShare />
|
||||
<WrapstodonSharedPage />
|
||||
</Router>
|
||||
</ReduxProvider>
|
||||
</IntlProvider>,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Button } from '@/flavours/glitch/components/button';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
@@ -12,7 +14,7 @@ export const AnnualReportAnnouncement: React.FC<{
|
||||
onOpen: () => void;
|
||||
}> = ({ year, hasData, isLoading, onRequestBuild, onOpen }) => {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={classNames('theme-dark', styles.wrapper)}>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='annual_report.announcement.title'
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-on-media);
|
||||
background: var(--color-bg-media-base);
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-primary);
|
||||
background:
|
||||
radial-gradient(at 40% 87%, #240c9a99 0, transparent 50%),
|
||||
radial-gradient(at 19% 10%, #6b0c9a99 0, transparent 50%),
|
||||
radial-gradient(at 90% 27%, #9a0c8299 0, transparent 50%),
|
||||
radial-gradient(at 16% 95%, #1e948299 0, transparent 50%)
|
||||
var(--color-bg-media-base);
|
||||
var(--color-bg-primary);
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
|
||||
h2 {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import {
|
||||
accountFactoryState,
|
||||
annualReportFactory,
|
||||
statusFactoryState,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import { AnnualReport } from '.';
|
||||
|
||||
const SAMPLE_HASHTAG = {
|
||||
name: 'Mastodon',
|
||||
count: 14,
|
||||
};
|
||||
|
||||
const meta = {
|
||||
title: 'Components/AnnualReport',
|
||||
component: AnnualReport,
|
||||
args: {
|
||||
context: 'standalone',
|
||||
},
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState({ display_name: 'Freddie Fruitbat' }),
|
||||
},
|
||||
statuses: {
|
||||
'1': statusFactoryState(),
|
||||
},
|
||||
annualReport: annualReportFactory({
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof AnnualReport>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Standalone: Story = {
|
||||
args: {
|
||||
context: 'standalone',
|
||||
},
|
||||
};
|
||||
|
||||
export const InModal: Story = {
|
||||
args: {
|
||||
context: 'modal',
|
||||
},
|
||||
};
|
||||
|
||||
export const ArchetypeOracle: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'oracle',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'booster',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoNewPosts: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'pollster',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
without_posts: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoNewPostsNoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'replier',
|
||||
without_posts: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,65 +1,214 @@
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import type { Archetype as ArchetypeData } from '@/flavours/glitch/models/annual_report';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Avatar } from '@/flavours/glitch/components/avatar';
|
||||
import { Button } from '@/flavours/glitch/components/button';
|
||||
import type { Account } from '@/flavours/glitch/models/account';
|
||||
import type {
|
||||
AnnualReport,
|
||||
Archetype as ArchetypeData,
|
||||
} from '@/flavours/glitch/models/annual_report';
|
||||
import booster from '@/images/archetypes/booster.png';
|
||||
import lurker from '@/images/archetypes/lurker.png';
|
||||
import oracle from '@/images/archetypes/oracle.png';
|
||||
import pollster from '@/images/archetypes/pollster.png';
|
||||
import replier from '@/images/archetypes/replier.png';
|
||||
import space_elements from '@/images/archetypes/space_elements.png';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { ShareButton } from './share_button';
|
||||
|
||||
export const archetypeNames = defineMessages<ArchetypeData>({
|
||||
booster: {
|
||||
id: 'annual_report.summary.archetype.booster',
|
||||
defaultMessage: 'The cool-hunter',
|
||||
id: 'annual_report.summary.archetype.booster.name',
|
||||
defaultMessage: 'The Archer',
|
||||
},
|
||||
replier: {
|
||||
id: 'annual_report.summary.archetype.replier',
|
||||
defaultMessage: 'The social butterfly',
|
||||
id: 'annual_report.summary.archetype.replier.name',
|
||||
defaultMessage: 'The Butterfly',
|
||||
},
|
||||
pollster: {
|
||||
id: 'annual_report.summary.archetype.pollster',
|
||||
defaultMessage: 'The pollster',
|
||||
id: 'annual_report.summary.archetype.pollster.name',
|
||||
defaultMessage: 'The Wonderer',
|
||||
},
|
||||
lurker: {
|
||||
id: 'annual_report.summary.archetype.lurker',
|
||||
defaultMessage: 'The lurker',
|
||||
id: 'annual_report.summary.archetype.lurker.name',
|
||||
defaultMessage: 'The Stoic',
|
||||
},
|
||||
oracle: {
|
||||
id: 'annual_report.summary.archetype.oracle',
|
||||
defaultMessage: 'The oracle',
|
||||
id: 'annual_report.summary.archetype.oracle.name',
|
||||
defaultMessage: 'The Oracle',
|
||||
},
|
||||
});
|
||||
|
||||
export const Archetype: React.FC<{
|
||||
data: ArchetypeData;
|
||||
}> = ({ data }) => {
|
||||
const intl = useIntl();
|
||||
let illustration;
|
||||
export const archetypeSelfDescriptions = defineMessages<ArchetypeData>({
|
||||
booster: {
|
||||
id: 'annual_report.summary.archetype.booster.desc_self',
|
||||
defaultMessage:
|
||||
'You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.',
|
||||
},
|
||||
replier: {
|
||||
id: 'annual_report.summary.archetype.replier.desc_self',
|
||||
defaultMessage:
|
||||
'You frequently replied to other people’s posts, pollinating Mastodon with new discussions.',
|
||||
},
|
||||
pollster: {
|
||||
id: 'annual_report.summary.archetype.pollster.desc_self',
|
||||
defaultMessage:
|
||||
'You created more polls than other post types, cultivating curiosity on Mastodon.',
|
||||
},
|
||||
lurker: {
|
||||
id: 'annual_report.summary.archetype.lurker.desc_self',
|
||||
defaultMessage:
|
||||
'We know you were out there, somewhere, enjoying Mastodon in your own quiet way.',
|
||||
},
|
||||
oracle: {
|
||||
id: 'annual_report.summary.archetype.oracle.desc_self',
|
||||
defaultMessage:
|
||||
'You created new posts more than replies, keeping Mastodon fresh and future-facing.',
|
||||
},
|
||||
});
|
||||
|
||||
switch (data) {
|
||||
case 'booster':
|
||||
illustration = booster;
|
||||
break;
|
||||
case 'replier':
|
||||
illustration = replier;
|
||||
break;
|
||||
case 'pollster':
|
||||
illustration = pollster;
|
||||
break;
|
||||
case 'lurker':
|
||||
illustration = lurker;
|
||||
break;
|
||||
case 'oracle':
|
||||
illustration = oracle;
|
||||
break;
|
||||
}
|
||||
export const archetypePublicDescriptions = defineMessages<ArchetypeData>({
|
||||
booster: {
|
||||
id: 'annual_report.summary.archetype.booster.desc_public',
|
||||
defaultMessage:
|
||||
'{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.',
|
||||
},
|
||||
replier: {
|
||||
id: 'annual_report.summary.archetype.replier.desc_public',
|
||||
defaultMessage:
|
||||
'{name} frequently replied to other people’s posts, pollinating Mastodon with new discussions.',
|
||||
},
|
||||
pollster: {
|
||||
id: 'annual_report.summary.archetype.pollster.desc_public',
|
||||
defaultMessage:
|
||||
'{name} created more polls than other post types, cultivating curiosity on Mastodon.',
|
||||
},
|
||||
lurker: {
|
||||
id: 'annual_report.summary.archetype.lurker.desc_public',
|
||||
defaultMessage:
|
||||
'We know {name} was out there, somewhere, enjoying Mastodon in their own quiet way.',
|
||||
},
|
||||
oracle: {
|
||||
id: 'annual_report.summary.archetype.oracle.desc_public',
|
||||
defaultMessage:
|
||||
'{name} created new posts more than replies, keeping Mastodon fresh and future-facing.',
|
||||
},
|
||||
});
|
||||
|
||||
const illustrations = {
|
||||
booster,
|
||||
replier,
|
||||
pollster,
|
||||
lurker,
|
||||
oracle,
|
||||
} as const;
|
||||
|
||||
export const Archetype: React.FC<{
|
||||
report: AnnualReport;
|
||||
account?: Account;
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ report, account, context }) => {
|
||||
const intl = useIntl();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const isSelfView = context === 'modal';
|
||||
|
||||
const [isRevealed, setIsRevealed] = useState(!isSelfView);
|
||||
const reveal = useCallback(() => {
|
||||
setIsRevealed(true);
|
||||
wrapperRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const archetype = report.data.archetype;
|
||||
const descriptions = isSelfView
|
||||
? archetypeSelfDescriptions
|
||||
: archetypePublicDescriptions;
|
||||
|
||||
const name = account?.display_name;
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__archetype'>
|
||||
<div className='annual-report__summary__archetype__label'>
|
||||
{intl.formatMessage(archetypeNames[data])}
|
||||
<div
|
||||
className={classNames(styles.box, styles.archetype)}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={0}
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<div className={styles.archetypeArtboard}>
|
||||
{account && (
|
||||
<Avatar
|
||||
account={account}
|
||||
size={50}
|
||||
className={styles.archetypeAvatar}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.archetypeIllustrationWrapper}>
|
||||
<img
|
||||
src={illustrations[archetype]}
|
||||
alt=''
|
||||
className={classNames(
|
||||
styles.archetypeIllustration,
|
||||
isRevealed ? '' : styles.blurredImage,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src={space_elements}
|
||||
alt=''
|
||||
className={styles.archetypePlanetRing}
|
||||
/>
|
||||
</div>
|
||||
<img src={illustration} alt='' />
|
||||
<div className={classNames(styles.content, styles.comfortable)}>
|
||||
<h2 className={styles.title}>
|
||||
{isSelfView ? (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.title_self'
|
||||
defaultMessage='Your archetype'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.title_public'
|
||||
defaultMessage="{name}'s archetype"
|
||||
values={{ name }}
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
<p className={styles.statLarge}>
|
||||
{isRevealed ? (
|
||||
intl.formatMessage(archetypeNames[archetype])
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.die_drei_fragezeichen'
|
||||
defaultMessage='???'
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{isRevealed ? (
|
||||
intl.formatMessage(descriptions[archetype], {
|
||||
name,
|
||||
})
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.reveal_description'
|
||||
defaultMessage='Thanks for being part of Mastodon! Time to find out which archetype you embodied in {year}.'
|
||||
values={{ year: report.year }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{!isRevealed && (
|
||||
<Button onClick={reveal}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.reveal'
|
||||
defaultMessage='Reveal my archetype'
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{isRevealed && isSelfView && <ShareButton report={report} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,68 +1,24 @@
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
|
||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import type { TimeSeriesMonth } from 'flavours/glitch/models/annual_report';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
export const Followers: React.FC<{
|
||||
data: TimeSeriesMonth[];
|
||||
total?: number;
|
||||
}> = ({ data, total }) => {
|
||||
const change = data.reduce((sum, item) => sum + item.followers, 0);
|
||||
|
||||
const cumulativeGraph = data.reduce(
|
||||
(newData, item) => [
|
||||
...newData,
|
||||
item.followers + (newData[newData.length - 1] ?? 0),
|
||||
],
|
||||
[0],
|
||||
);
|
||||
|
||||
count: number;
|
||||
}> = ({ count }) => {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__followers'>
|
||||
<Sparklines data={cumulativeGraph} margin={0}>
|
||||
<svg>
|
||||
<defs>
|
||||
<linearGradient id='gradient' x1='0%' y1='0%' x2='0%' y2='100%'>
|
||||
<stop
|
||||
offset='0%'
|
||||
stopColor='var(--sparkline-gradient-top)'
|
||||
stopOpacity='1'
|
||||
/>
|
||||
<stop
|
||||
offset='100%'
|
||||
stopColor='var(--sparkline-gradient-bottom)'
|
||||
stopOpacity='0'
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div className={classNames(styles.box, styles.followers, styles.content)}>
|
||||
<div className={styles.statLarge}>
|
||||
<FormattedNumber value={count} />
|
||||
</div>
|
||||
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
|
||||
<div className='annual-report__summary__followers__foreground'>
|
||||
<div className='annual-report__summary__followers__number'>
|
||||
{change > -1 ? '+' : '-'}
|
||||
<FormattedNumber value={change} />
|
||||
</div>
|
||||
|
||||
<div className='annual-report__summary__followers__label'>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.followers.followers'
|
||||
defaultMessage='followers'
|
||||
/>
|
||||
</span>
|
||||
<div className='annual-report__summary__followers__footnote'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.followers.total'
|
||||
defaultMessage='{count} total'
|
||||
values={{ count: <ShortNumber value={total ?? 0} /> }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.followers.new_followers'
|
||||
defaultMessage='{count, plural, one {new follower} other {new followers}}'
|
||||
values={{ count }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,106 +1,102 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return,
|
||||
@typescript-eslint/no-explicit-any,
|
||||
@typescript-eslint/no-unsafe-assignment */
|
||||
@typescript-eslint/no-unsafe-assignment,
|
||||
@typescript-eslint/no-unsafe-member-access,
|
||||
@typescript-eslint/no-unsafe-call */
|
||||
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { DisplayName } from '@/flavours/glitch/components/display_name';
|
||||
import { toggleStatusSpoilers } from 'flavours/glitch/actions/statuses';
|
||||
import { DetailedStatus } from 'flavours/glitch/features/status/components/detailed_status';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { InterceptStatusClicks } from 'flavours/glitch/components/status/intercept_status_clicks';
|
||||
import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted';
|
||||
import type { TopStatuses } from 'flavours/glitch/models/annual_report';
|
||||
import {
|
||||
makeGetStatus,
|
||||
makeGetPictureInPicture,
|
||||
} from 'flavours/glitch/selectors';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
import { useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
|
||||
const getPictureInPicture = makeGetPictureInPicture() as unknown as (
|
||||
arg0: any,
|
||||
arg1: any,
|
||||
) => any;
|
||||
|
||||
export const HighlightedPost: React.FC<{
|
||||
data: TopStatuses;
|
||||
}> = ({ data }) => {
|
||||
let statusId, label;
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ data, context }) => {
|
||||
const { by_reblogs, by_favourites, by_replies } = data;
|
||||
|
||||
if (data.by_reblogs) {
|
||||
statusId = data.by_reblogs;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.by_reblogs'
|
||||
defaultMessage='most boosted post'
|
||||
/>
|
||||
);
|
||||
} else if (data.by_favourites) {
|
||||
statusId = data.by_favourites;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.by_favourites'
|
||||
defaultMessage='most favourited post'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
statusId = data.by_replies;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.by_replies'
|
||||
defaultMessage='post with the most replies'
|
||||
/>
|
||||
);
|
||||
}
|
||||
const statusId = by_reblogs || by_favourites || by_replies;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const domain = useAppSelector((state) => state.meta.get('domain'));
|
||||
const status = useAppSelector((state) =>
|
||||
statusId ? getStatus(state, { id: statusId }) : undefined,
|
||||
);
|
||||
const pictureInPicture = useAppSelector((state) =>
|
||||
statusId ? getPictureInPicture(state, { id: statusId }) : undefined,
|
||||
);
|
||||
const account = useAppSelector((state) =>
|
||||
me ? state.accounts.get(me) : undefined,
|
||||
);
|
||||
|
||||
const handleToggleHidden = useCallback(() => {
|
||||
dispatch(toggleStatusSpoilers(statusId));
|
||||
}, [dispatch, statusId]);
|
||||
const handleClick = useCallback<
|
||||
ComponentPropsWithoutRef<typeof InterceptStatusClicks>['onPreventedClick']
|
||||
>(
|
||||
(clickedArea) => {
|
||||
const link: string =
|
||||
clickedArea === 'account'
|
||||
? status.getIn(['account', 'url'])
|
||||
: status.get('url');
|
||||
|
||||
if (context === 'standalone') {
|
||||
window.location.href = link;
|
||||
} else {
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
},
|
||||
[status, context],
|
||||
);
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-boosted-post' />
|
||||
return <div className={classNames(styles.box, styles.mostBoostedPost)} />;
|
||||
}
|
||||
|
||||
let label;
|
||||
if (by_reblogs) {
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.boost_count'
|
||||
defaultMessage='This post was boosted {count, plural, one {once} other {# times}}.'
|
||||
values={{ count: status.get('reblogs_count') }}
|
||||
/>
|
||||
);
|
||||
} else if (by_favourites) {
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.favourite_count'
|
||||
defaultMessage='This post was favorited {count, plural, one {once} other {# times}}.'
|
||||
values={{ count: status.get('favourites_count') }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.reply_count'
|
||||
defaultMessage='This post got {count, plural, one {one reply} other {# replies}}.'
|
||||
values={{ count: status.get('replies_count') }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const displayName = (
|
||||
<span className='display-name'>
|
||||
<strong className='display-name__html'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.possessive'
|
||||
defaultMessage="{name}'s"
|
||||
values={{
|
||||
name: <DisplayName account={account} variant='simple' />,
|
||||
}}
|
||||
/>
|
||||
</strong>
|
||||
<span className='display-name__account'>{label}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-boosted-post'>
|
||||
<DetailedStatus
|
||||
status={status}
|
||||
pictureInPicture={pictureInPicture}
|
||||
domain={domain}
|
||||
onToggleHidden={handleToggleHidden}
|
||||
overrideDisplayName={displayName}
|
||||
expanded={false}
|
||||
/>
|
||||
<div className={classNames(styles.box, styles.mostBoostedPost)}>
|
||||
<div className={styles.content}>
|
||||
<h2 className={styles.title}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.title'
|
||||
defaultMessage='Most popular post'
|
||||
/>
|
||||
</h2>
|
||||
{context === 'modal' && <p>{label}</p>}
|
||||
</div>
|
||||
|
||||
<InterceptStatusClicks onPreventedClick={handleClick}>
|
||||
<StatusQuoteManager showActions={false} id={statusId} />
|
||||
</InterceptStatusClicks>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
$mobile-breakpoint: 540px;
|
||||
|
||||
@font-face {
|
||||
font-family: silkscreen-wrapstodon;
|
||||
src: url('@/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2')
|
||||
format('woff2');
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.modalWrapper {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary);
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.loading-indicator .circular-progress {
|
||||
color: var(--lime);
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
--default-icon-color: var(--color-bg-primary);
|
||||
--default-bg-color: var(--color-text-primary);
|
||||
--hover-icon-color: var(--color-bg-primary);
|
||||
--hover-bg-color: var(--color-text-primary);
|
||||
--corner-distance: 18px;
|
||||
|
||||
position: absolute;
|
||||
top: var(--corner-distance);
|
||||
right: var(--corner-distance);
|
||||
padding: 8px;
|
||||
border-radius: 100%;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
--corner-distance: 16px;
|
||||
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
--gradient-strength: 0.4;
|
||||
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
max-width: 600px;
|
||||
padding: 24px;
|
||||
padding-top: 40px;
|
||||
contain: layout;
|
||||
flex: 0 0 auto;
|
||||
pointer-events: all;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-primary);
|
||||
background:
|
||||
radial-gradient(
|
||||
at 10% 27%,
|
||||
rgba(83, 12, 154, var(--gradient-strength)) 0,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
at 91% 10%,
|
||||
rgba(30, 24, 223, var(--gradient-strength)) 0,
|
||||
transparent 25%
|
||||
),
|
||||
radial-gradient(
|
||||
at 10% 91%,
|
||||
rgba(22, 218, 228, var(--gradient-strength)) 0,
|
||||
transparent 40%
|
||||
),
|
||||
radial-gradient(
|
||||
at 75% 87%,
|
||||
rgba(37, 31, 217, var(--gradient-strength)) 0,
|
||||
transparent 20%
|
||||
),
|
||||
radial-gradient(
|
||||
at 84% 60%,
|
||||
rgba(95, 30, 148, var(--gradient-strength)) 0,
|
||||
transparent 40%
|
||||
)
|
||||
var(--color-bg-primary);
|
||||
border-radius: 40px;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding-inline: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 18px;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-family: silkscreen-wrapstodon, monospace;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
padding-inline: 40px; // Prevent overlap with close button
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
font-size: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.stack {
|
||||
--grid-spacing: 12px;
|
||||
|
||||
display: grid;
|
||||
gap: var(--grid-spacing);
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
background: rgb(from var(--color-bg-primary) r g b / 60%);
|
||||
box-shadow: inset 0 0 0 1px rgb(from var(--color-text-primary) r g b / 40%);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
white,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
|
||||
&.comfortable {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
color: #c2c8ff;
|
||||
font-weight: 500;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.statLarge {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.statExtraLarge {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.mostBoostedPost {
|
||||
padding: 0;
|
||||
padding-top: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
gap: var(--grid-spacing);
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-areas:
|
||||
'followers hashtag'
|
||||
'new-posts hashtag';
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas:
|
||||
'followers new-posts'
|
||||
'hashtag hashtag';
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.onlyHashtag {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas: 'hashtag';
|
||||
}
|
||||
|
||||
&.noHashtag {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas: 'followers new-posts';
|
||||
}
|
||||
|
||||
&.singleNumber {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-areas: 'number hashtag';
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
grid-template-areas:
|
||||
'number number'
|
||||
'hashtag hashtag';
|
||||
}
|
||||
}
|
||||
|
||||
&.singleNumber.noHashtag {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas: 'number';
|
||||
}
|
||||
}
|
||||
|
||||
.followers {
|
||||
grid-area: followers;
|
||||
|
||||
.singleNumber & {
|
||||
grid-area: number;
|
||||
}
|
||||
}
|
||||
|
||||
.newPosts {
|
||||
grid-area: new-posts;
|
||||
|
||||
.singleNumber & {
|
||||
grid-area: number;
|
||||
}
|
||||
}
|
||||
|
||||
.mostUsedHashtag {
|
||||
grid-area: hashtag;
|
||||
padding-block: 24px;
|
||||
}
|
||||
|
||||
.archetype {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
max-width: 460px;
|
||||
}
|
||||
}
|
||||
|
||||
.archetypeArtboard {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
width: 180px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.archetypeAvatar {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 4px;
|
||||
border-radius: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.archetypeIllustrationWrapper {
|
||||
position: relative;
|
||||
width: 92px;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 100%;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: inset -10px -4px 15px #00000080;
|
||||
}
|
||||
}
|
||||
|
||||
.archetypeIllustration {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.blurredImage {
|
||||
filter: blur(10px);
|
||||
}
|
||||
|
||||
.archetypePlanetRing {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
@@ -1,95 +1,112 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import { focusCompose, resetCompose } from '@/flavours/glitch/actions/compose';
|
||||
import { closeModal } from '@/flavours/glitch/actions/modal';
|
||||
import { Button } from '@/flavours/glitch/components/button';
|
||||
import { IconButton } from '@/flavours/glitch/components/icon_button';
|
||||
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
|
||||
import { me } from '@/flavours/glitch/initial_state';
|
||||
import type { AnnualReport as AnnualReportData } from '@/flavours/glitch/models/annual_report';
|
||||
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { Archetype, archetypeNames } from './archetype';
|
||||
import { Archetype } from './archetype';
|
||||
import { Followers } from './followers';
|
||||
import { HighlightedPost } from './highlighted_post';
|
||||
import styles from './index.module.scss';
|
||||
import { MostUsedHashtag } from './most_used_hashtag';
|
||||
import { NewPosts } from './new_posts';
|
||||
|
||||
const shareMessage = defineMessage({
|
||||
id: 'annual_report.summary.share_message',
|
||||
defaultMessage: 'I got the {archetype} archetype!',
|
||||
});
|
||||
const moduleClassNames = classNames.bind(styles);
|
||||
|
||||
// Share = false when using the embedded version of the report.
|
||||
export const AnnualReport: FC<{ share?: boolean }> = ({ share = true }) => {
|
||||
const currentAccount = useAppSelector((state) =>
|
||||
me ? state.accounts.get(me) : undefined,
|
||||
);
|
||||
export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
context = 'standalone',
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const report = useAppSelector((state) => state.annualReport.report);
|
||||
const account = useAppSelector((state) => {
|
||||
if (me) {
|
||||
return state.accounts.get(me);
|
||||
}
|
||||
if (report?.schema_version === 2) {
|
||||
return state.accounts.get(report.account_id);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const close = useCallback(() => {
|
||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||
}, [dispatch]);
|
||||
|
||||
// Close modal when navigating away from within
|
||||
const { pathname } = useLocation();
|
||||
const [initialPathname] = useState(pathname);
|
||||
useEffect(() => {
|
||||
if (pathname !== initialPathname) {
|
||||
close();
|
||||
}
|
||||
}, [pathname, initialPathname, close]);
|
||||
|
||||
if (!report) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
const newPostCount = report.data.time_series.reduce(
|
||||
(sum, item) => sum + item.statuses,
|
||||
0,
|
||||
);
|
||||
|
||||
const newFollowerCount =
|
||||
context === 'modal' &&
|
||||
report.data.time_series.reduce((sum, item) => sum + item.followers, 0);
|
||||
|
||||
const topHashtag = report.data.top_hashtags[0];
|
||||
|
||||
return (
|
||||
<div className='annual-report'>
|
||||
<div className='annual-report__header'>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.thanks'
|
||||
defaultMessage='Thanks for being part of Mastodon!'
|
||||
<div className={moduleClassNames(styles.wrapper, 'theme-dark')}>
|
||||
<div className={styles.header}>
|
||||
<h1>Wrapstodon {report.year}</h1>
|
||||
{account && <p>@{account.acct}</p>}
|
||||
{context === 'modal' && (
|
||||
<IconButton
|
||||
title={intl.formatMessage({
|
||||
id: 'annual_report.summary.close',
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
className={styles.closeButton}
|
||||
icon='close'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={close}
|
||||
/>
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.here_it_is'
|
||||
defaultMessage='Here is your {year} in review:'
|
||||
values={{ year: report.year }}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='annual-report__bento annual-report__summary'>
|
||||
<Archetype data={report.data.archetype} />
|
||||
<HighlightedPost data={report.data.top_statuses} />
|
||||
<Followers
|
||||
data={report.data.time_series}
|
||||
total={currentAccount?.followers_count}
|
||||
/>
|
||||
<MostUsedHashtag data={report.data.top_hashtags} />
|
||||
<NewPosts data={report.data.time_series} />
|
||||
{share && <ShareButton report={report} />}
|
||||
<div className={styles.stack}>
|
||||
<HighlightedPost data={report.data.top_statuses} context={context} />
|
||||
<div
|
||||
className={moduleClassNames(styles.statsGrid, {
|
||||
noHashtag: !topHashtag,
|
||||
onlyHashtag: !(newFollowerCount && newPostCount),
|
||||
singleNumber: !!newFollowerCount !== !!newPostCount,
|
||||
})}
|
||||
>
|
||||
{!!newFollowerCount && <Followers count={newFollowerCount} />}
|
||||
{!!newPostCount && <NewPosts count={newPostCount} />}
|
||||
{topHashtag && (
|
||||
<MostUsedHashtag
|
||||
hashtag={topHashtag}
|
||||
name={account?.display_name}
|
||||
context={context}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Archetype report={report} account={account} context={context} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const handleShareClick = useCallback(() => {
|
||||
// Generate the share message.
|
||||
const archetypeName = intl.formatMessage(
|
||||
archetypeNames[report.data.archetype],
|
||||
);
|
||||
const shareLines = [
|
||||
intl.formatMessage(shareMessage, {
|
||||
archetype: archetypeName,
|
||||
}),
|
||||
];
|
||||
// Share URL is only available for schema version 2.
|
||||
if (report.schema_version === 2 && report.share_url) {
|
||||
shareLines.push(report.share_url);
|
||||
}
|
||||
shareLines.push(`#Wrapstodon${report.year}`);
|
||||
|
||||
// Reset the composer and focus it with the share message, then close the modal.
|
||||
dispatch(resetCompose());
|
||||
dispatch(focusCompose(shareLines.join('\n\n')));
|
||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||
}, [report, intl, dispatch]);
|
||||
|
||||
return <Button text='Share here' onClick={handleShareClick} />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { closeModal } from '@/flavours/glitch/actions/modal';
|
||||
import { useAppDispatch } from '@/flavours/glitch/store';
|
||||
|
||||
import { AnnualReport } from '.';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const AnnualReportModal: React.FC<{
|
||||
onChangeBackgroundColor: (color: string) => void;
|
||||
}> = ({ onChangeBackgroundColor }) => {
|
||||
useEffect(() => {
|
||||
onChangeBackgroundColor('var(--color-bg-media-base)');
|
||||
}, [onChangeBackgroundColor]);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleCloseModal = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
||||
(e) => {
|
||||
if (e.target === e.currentTarget)
|
||||
dispatch(
|
||||
closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
// It's fine not to provide a keyboard handler here since there is a global
|
||||
// [Esc] key listener that will close open modals.
|
||||
// This onClick handler is needed since the modalWrapper styles overlap the
|
||||
// default modal backdrop, preventing clicks to pass through.
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={classNames(
|
||||
'modal-root__modal',
|
||||
styles.modalWrapper,
|
||||
'theme-dark',
|
||||
)}
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
<AnnualReport context='modal' />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AnnualReportModal;
|
||||
@@ -1,30 +1,46 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { NameAndCount } from 'flavours/glitch/models/annual_report';
|
||||
|
||||
export const MostUsedHashtag: React.FC<{
|
||||
data: NameAndCount[];
|
||||
}> = ({ data }) => {
|
||||
const hashtag = data[0];
|
||||
import styles from './index.module.scss';
|
||||
|
||||
export const MostUsedHashtag: React.FC<{
|
||||
hashtag: NameAndCount;
|
||||
name: string | undefined;
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ hashtag, name, context }) => {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-used-hashtag'>
|
||||
<div className='annual-report__summary__most-used-hashtag__hashtag'>
|
||||
{hashtag ? (
|
||||
<>#{hashtag.name}</>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.none'
|
||||
defaultMessage='None'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='annual-report__summary__most-used-hashtag__label'>
|
||||
<div
|
||||
className={classNames(styles.box, styles.mostUsedHashtag, styles.content)}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.most_used_hashtag'
|
||||
defaultMessage='most used hashtag'
|
||||
defaultMessage='Most used hashtag'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.statExtraLarge}>#{hashtag.name}</div>
|
||||
|
||||
<p>
|
||||
{context === 'modal' ? (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count'
|
||||
defaultMessage='You included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count }}
|
||||
/>
|
||||
) : (
|
||||
name && (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count_public'
|
||||
defaultMessage='{name} included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count, name }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,51 +1,23 @@
|
||||
import { FormattedNumber, FormattedMessage } from 'react-intl';
|
||||
|
||||
import ChatBubbleIcon from '@/material-icons/400-24px/chat_bubble.svg?react';
|
||||
import type { TimeSeriesMonth } from 'flavours/glitch/models/annual_report';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
export const NewPosts: React.FC<{
|
||||
data: TimeSeriesMonth[];
|
||||
}> = ({ data }) => {
|
||||
const posts = data.reduce((sum, item) => sum + item.statuses, 0);
|
||||
|
||||
count: number;
|
||||
}> = ({ count }) => {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__new-posts'>
|
||||
<svg width={500} height={500}>
|
||||
<defs>
|
||||
<pattern
|
||||
id='posts'
|
||||
x='0'
|
||||
y='0'
|
||||
width='32'
|
||||
height='35'
|
||||
patternUnits='userSpaceOnUse'
|
||||
>
|
||||
<circle cx='12' cy='12' r='12' fill='var(--lime)' />
|
||||
<ChatBubbleIcon
|
||||
fill='var(--indigo-1)'
|
||||
x='4'
|
||||
y='4'
|
||||
width='16'
|
||||
height='16'
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect
|
||||
width={500}
|
||||
height={500}
|
||||
fill='url(#posts)'
|
||||
style={{ opacity: 0.2 }}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className='annual-report__summary__new-posts__number'>
|
||||
<FormattedNumber value={posts} />
|
||||
<div className={classNames(styles.box, styles.newPosts, styles.content)}>
|
||||
<div className={styles.statLarge}>
|
||||
<FormattedNumber value={count} />
|
||||
</div>
|
||||
<div className='annual-report__summary__new-posts__label'>
|
||||
|
||||
<div className={styles.title}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.new_posts.new_posts'
|
||||
defaultMessage='new posts'
|
||||
defaultMessage='{count, plural, one {new post} other {new posts}}'
|
||||
values={{ count }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.wrapper {
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { IconLogo } from '@/flavours/glitch/components/logo';
|
||||
|
||||
import { AnnualReport } from './index';
|
||||
import classes from './share.module.css';
|
||||
|
||||
export const WrapstodonShare: FC = () => {
|
||||
return (
|
||||
<main className={classes.wrapper}>
|
||||
<AnnualReport share={false} />
|
||||
<footer className={classes.footer}>
|
||||
<IconLogo className={classes.logo} />
|
||||
Generated with ♥ by the Mastodon team
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { resetCompose, focusCompose } from '@/flavours/glitch/actions/compose';
|
||||
import { closeModal } from '@/flavours/glitch/actions/modal';
|
||||
import { Button } from '@/flavours/glitch/components/button';
|
||||
import type { AnnualReport as AnnualReportData } from '@/flavours/glitch/models/annual_report';
|
||||
import { useAppDispatch } from '@/flavours/glitch/store';
|
||||
|
||||
import { archetypeNames } from './archetype';
|
||||
|
||||
const messages = defineMessages({
|
||||
share_message: {
|
||||
id: 'annual_report.summary.share_message',
|
||||
defaultMessage: 'I got the {archetype} archetype!',
|
||||
},
|
||||
share_on_mastodon: {
|
||||
id: 'annual_report.summary.share_on_mastodon',
|
||||
defaultMessage: 'Share on Mastodon',
|
||||
},
|
||||
});
|
||||
|
||||
export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const handleShareClick = useCallback(() => {
|
||||
// Generate the share message.
|
||||
const archetypeName = intl.formatMessage(
|
||||
archetypeNames[report.data.archetype],
|
||||
);
|
||||
const shareLines = [
|
||||
intl.formatMessage(messages.share_message, {
|
||||
archetype: archetypeName,
|
||||
}),
|
||||
];
|
||||
// Share URL is only available for schema version 2.
|
||||
if (report.schema_version === 2 && report.share_url) {
|
||||
shareLines.push(report.share_url);
|
||||
}
|
||||
shareLines.push(`#Wrapstodon${report.year}`);
|
||||
|
||||
// Reset the composer and focus it with the share message, then close the modal.
|
||||
dispatch(resetCompose());
|
||||
dispatch(focusCompose(shareLines.join('\n\n')));
|
||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||
}, [report, intl, dispatch]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.share_on_mastodon)}
|
||||
onClick={handleShareClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
$mobile-breakpoint: 540px;
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
max-width: 600px;
|
||||
margin-inline: auto;
|
||||
padding: 40px 10px;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding-top: 0;
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2rem;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
a:any-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { IconLogo } from '@/flavours/glitch/components/logo';
|
||||
import { me } from '@/flavours/glitch/initial_state';
|
||||
|
||||
import { AnnualReport } from './index';
|
||||
import classes from './shared_page.module.scss';
|
||||
|
||||
export const WrapstodonSharedPage: FC = () => {
|
||||
return (
|
||||
<main className={classes.wrapper}>
|
||||
<AnnualReport />
|
||||
<footer className={classes.footer}>
|
||||
<IconLogo className={classes.logo} />
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.footer'
|
||||
defaultMessage='Generated with {heart} by the Mastodon team'
|
||||
values={{ heart: '♥' }}
|
||||
/>
|
||||
<nav className={classes.nav}>
|
||||
<a href='/about'>
|
||||
<FormattedMessage
|
||||
id='footer.about_this_server'
|
||||
defaultMessage='About'
|
||||
/>
|
||||
</a>
|
||||
{!me && (
|
||||
<a href='https://joinmastodon.org/servers'>
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.sign_up'
|
||||
defaultMessage='Sign up'
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<a href='https://joinmastodon.org/sponsors'>
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.donate'
|
||||
defaultMessage='Donate'
|
||||
/>
|
||||
</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { AnnualReport } from 'flavours/glitch/features/annual_report';
|
||||
|
||||
const AnnualReportModal: React.FC<{
|
||||
onChangeBackgroundColor: (color: string) => void;
|
||||
}> = ({ onChangeBackgroundColor }) => {
|
||||
useEffect(() => {
|
||||
onChangeBackgroundColor('var(--indigo-1)');
|
||||
}, [onChangeBackgroundColor]);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal annual-report-modal'>
|
||||
<AnnualReport />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AnnualReportModal;
|
||||
@@ -4,10 +4,10 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { scrollTopTimeline, loadPending, TIMELINE_SUGGESTIONS } from '@/flavours/glitch/actions/timelines';
|
||||
import { scrollTopTimeline, loadPending } from '@/flavours/glitch/actions/timelines';
|
||||
import { isNonStatusId } from '@/flavours/glitch/actions/timelines_typed';
|
||||
import StatusList from '@/flavours/glitch/components/status_list';
|
||||
import { me } from '@/flavours/glitch/initial_state';
|
||||
import { TIMELINE_WRAPSTODON } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||
|
||||
const getRegex = createSelector([
|
||||
(state, { regex }) => regex,
|
||||
@@ -29,7 +29,7 @@ const makeGetStatusIds = (pending = false) => createSelector([
|
||||
getRegex,
|
||||
], (columnSettings, statusIds, statuses, regex) => {
|
||||
return statusIds.filter(id => {
|
||||
if (id === null || id === TIMELINE_SUGGESTIONS || id === TIMELINE_WRAPSTODON) return true;
|
||||
if (isNonStatusId(id)) return true;
|
||||
|
||||
const statusForId = statuses.get(id);
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ export function LinkTimeline () {
|
||||
}
|
||||
|
||||
export function AnnualReportModal () {
|
||||
return import('../components/annual_report_modal');
|
||||
return import('../../annual_report/modal');
|
||||
}
|
||||
|
||||
export function ListEdit () {
|
||||
|
||||
@@ -16,9 +16,9 @@ export interface TimeSeriesMonth {
|
||||
}
|
||||
|
||||
export interface TopStatuses {
|
||||
by_reblogs: number;
|
||||
by_favourites: number;
|
||||
by_replies: number;
|
||||
by_reblogs: string;
|
||||
by_favourites: string;
|
||||
by_replies: string;
|
||||
}
|
||||
|
||||
export type Archetype =
|
||||
@@ -55,5 +55,6 @@ export type AnnualReport = {
|
||||
schema_version: 2;
|
||||
data: AnnualReportV2;
|
||||
share_url: string | null;
|
||||
account_id: string;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
import type { ApiReportJSON } from 'flavours/glitch/api_types/reports';
|
||||
|
||||
// Maximum number of avatars displayed in a notification group
|
||||
// This corresponds to the max lenght of `group.sampleAccountIds`
|
||||
// This corresponds to the max length of `group.sampleAccountIds`
|
||||
export const NOTIFICATIONS_GROUP_MAX_AVATARS = 8;
|
||||
|
||||
interface BaseNotificationGroup
|
||||
|
||||
@@ -442,7 +442,9 @@ export const composeReducer = (state = initialState, action) => {
|
||||
|
||||
switch(action.type) {
|
||||
case STORE_HYDRATE:
|
||||
return hydrate(state, action.state.get('compose'));
|
||||
if (action.state.get('compose'))
|
||||
return hydrate(state, action.state.get('compose'));
|
||||
return state;
|
||||
case COMPOSE_MOUNT:
|
||||
return state
|
||||
.set('mounted', state.get('mounted') + 1)
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
importFetchedStatuses,
|
||||
} from '@/flavours/glitch/actions/importer';
|
||||
import { insertIntoTimeline } from '@/flavours/glitch/actions/timelines';
|
||||
import { timelineDelete } from '@/flavours/glitch/actions/timelines_typed';
|
||||
import type { ApiAnnualReportState } from '@/flavours/glitch/api/annual_report';
|
||||
import {
|
||||
apiGetAnnualReport,
|
||||
@@ -78,6 +79,25 @@ export const checkAnnualReport = createAppThunk(
|
||||
},
|
||||
);
|
||||
|
||||
export const reinsertAnnualReport = createAppThunk(
|
||||
`${annualReportSlice.name}/reinsertAnnualReport`,
|
||||
(_arg: unknown, { dispatch, getState }) => {
|
||||
dispatch(
|
||||
timelineDelete({
|
||||
statusId: TIMELINE_WRAPSTODON,
|
||||
accountId: '',
|
||||
references: [],
|
||||
reblogOf: null,
|
||||
}),
|
||||
);
|
||||
const { state } = getState().annualReport;
|
||||
if (!state || state === 'ineligible') {
|
||||
return;
|
||||
}
|
||||
dispatch(insertIntoTimeline('home', TIMELINE_WRAPSTODON, 1));
|
||||
},
|
||||
);
|
||||
|
||||
const fetchReportState = createDataLoadingThunk(
|
||||
`${annualReportSlice.name}/fetchReportState`,
|
||||
async (_arg: unknown, { getState }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
|
||||
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
|
||||
import { timelineDelete, isNonStatusId } from 'flavours/glitch/actions/timelines_typed';
|
||||
|
||||
import {
|
||||
blockAccountSuccess,
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
TIMELINE_MARK_AS_PARTIAL,
|
||||
TIMELINE_INSERT,
|
||||
TIMELINE_GAP,
|
||||
TIMELINE_SUGGESTIONS,
|
||||
disconnectTimeline,
|
||||
} from '../actions/timelines';
|
||||
import { compareId } from '../compare_id';
|
||||
@@ -36,7 +35,6 @@ const initialTimeline = ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
});
|
||||
|
||||
const isPlaceholder = value => value === TIMELINE_GAP || value === TIMELINE_SUGGESTIONS;
|
||||
|
||||
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
|
||||
// This method is pretty tricky because:
|
||||
@@ -69,20 +67,20 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
||||
// First, find the furthest (if properly sorted, oldest) item in the timeline that is
|
||||
// newer than the oldest fetched one, as it's most likely that it delimits the gap.
|
||||
// Start the gap *after* that item.
|
||||
const lastIndex = oldIds.findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.last()) >= 0) + 1;
|
||||
const lastIndex = oldIds.findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.last()) >= 0) + 1;
|
||||
|
||||
// Then, try to find the furthest (if properly sorted, oldest) item in the timeline that
|
||||
// is newer than the most recent fetched one, as it delimits a section comprised of only
|
||||
// items older or within `newIds` (or that were deleted from the server, so should be removed
|
||||
// anyway).
|
||||
// Stop the gap *after* that item.
|
||||
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.first()) > 0) + 1;
|
||||
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.first()) > 0) + 1;
|
||||
|
||||
let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => {
|
||||
// It is possible, though unlikely, that the slice we are replacing contains items older
|
||||
// than the elements we got from the API. Get them and add them back at the back of the
|
||||
// slice.
|
||||
const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isPlaceholder(id) && compareId(id, newIds.last()) < 0);
|
||||
const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isNonStatusId(id) && compareId(id, newIds.last()) < 0);
|
||||
insertedIds.union(olderIds);
|
||||
|
||||
// Make sure we aren't inserting duplicates
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
@use 'mastodon/polls';
|
||||
@use 'mastodon/modal';
|
||||
@use 'mastodon/emoji_picker';
|
||||
@use 'mastodon/annual_reports';
|
||||
@use 'mastodon/about';
|
||||
@use 'mastodon/tables';
|
||||
@use 'mastodon/admin';
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
@use 'variables' as *;
|
||||
|
||||
:root {
|
||||
--indigo-1: #17063b;
|
||||
--indigo-2: #2f0c7a;
|
||||
--indigo-3: #562cfc;
|
||||
--indigo-5: #858afa;
|
||||
--indigo-6: #cccfff;
|
||||
--lime: #baff3b;
|
||||
--goldenrod-2: #ffc954;
|
||||
}
|
||||
|
||||
.annual-report {
|
||||
flex: 0 0 auto;
|
||||
background: var(--indigo-1);
|
||||
padding: 24px;
|
||||
|
||||
&__header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
font-size: 25px;
|
||||
font-weight: 600;
|
||||
line-height: 30px;
|
||||
color: var(--lime);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: var(--indigo-6);
|
||||
}
|
||||
}
|
||||
|
||||
&__bento {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, auto) minmax(0, 1fr) minmax(0, auto) minmax(
|
||||
0,
|
||||
auto
|
||||
);
|
||||
|
||||
&__box {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--indigo-2);
|
||||
color: var(--indigo-5);
|
||||
}
|
||||
}
|
||||
|
||||
&__summary {
|
||||
&__most-boosted-post {
|
||||
grid-column: span 2;
|
||||
grid-row: span 2;
|
||||
padding: 0;
|
||||
|
||||
.status__content,
|
||||
.content-warning {
|
||||
color: var(--indigo-6);
|
||||
}
|
||||
|
||||
.detailed-status {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.content-warning {
|
||||
border: 0;
|
||||
background: var(--indigo-1);
|
||||
|
||||
.link-button {
|
||||
color: var(--indigo-5);
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-status__meta__line {
|
||||
border-bottom-color: var(--indigo-3);
|
||||
}
|
||||
|
||||
.detailed-status__meta {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detailed-status__meta,
|
||||
.poll__footer,
|
||||
.poll__link,
|
||||
.detailed-status .logo,
|
||||
.detailed-status__display-name {
|
||||
color: var(--indigo-5);
|
||||
}
|
||||
|
||||
.detailed-status__meta .animated-number,
|
||||
.detailed-status__display-name strong {
|
||||
color: var(--indigo-6);
|
||||
}
|
||||
|
||||
.poll__chart {
|
||||
background-color: var(--indigo-3);
|
||||
|
||||
&.leading {
|
||||
background-color: var(--goldenrod-2);
|
||||
}
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.hashtag-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__followers {
|
||||
grid-column: span 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-block-start: 24px;
|
||||
padding-block-end: 24px;
|
||||
|
||||
--sparkline-gradient-top: rgba(86, 44, 252, 50%);
|
||||
--sparkline-gradient-bottom: rgba(86, 44, 252, 0%);
|
||||
|
||||
&__foreground {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__number {
|
||||
font-size: 31px;
|
||||
font-weight: 600;
|
||||
line-height: 37px;
|
||||
color: var(--lime);
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 17px;
|
||||
color: var(--indigo-6);
|
||||
}
|
||||
|
||||
&__footnote {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-end: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
height: 70%;
|
||||
width: auto;
|
||||
|
||||
path:first-child {
|
||||
fill: url('#gradient') !important;
|
||||
fill-opacity: 1 !important;
|
||||
}
|
||||
|
||||
path:last-child {
|
||||
stroke: var(--color-graph-primary-stroke) !important;
|
||||
fill: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__archetype {
|
||||
grid-column: span 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
padding: 16px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--lime);
|
||||
}
|
||||
}
|
||||
|
||||
&__most-used-app {
|
||||
grid-column: span 1;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__label {
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--indigo-6);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--goldenrod-2);
|
||||
}
|
||||
}
|
||||
|
||||
&__percentile {
|
||||
grid-row: span 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
padding: 16px 8px;
|
||||
|
||||
&__label {
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
&__number {
|
||||
font-size: 54px;
|
||||
font-weight: 600;
|
||||
line-height: 73px;
|
||||
color: var(--goldenrod-2);
|
||||
}
|
||||
|
||||
&__footnote {
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&__new-posts {
|
||||
grid-column: span 2;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&__label {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: var(--indigo-6);
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__number {
|
||||
font-size: 76px;
|
||||
font-weight: 600;
|
||||
line-height: 91px;
|
||||
color: var(--goldenrod-2);
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
inset-inline-start: -7px;
|
||||
top: -4px;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__most-used-hashtag {
|
||||
grid-column: span 2;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
&__hashtag {
|
||||
font-size: 42px;
|
||||
font-weight: 600;
|
||||
line-height: 58px;
|
||||
color: var(--indigo-6);
|
||||
margin-inline-start: -100%;
|
||||
margin-inline-end: -100%;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 17px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.annual-report-modal {
|
||||
max-width: 600px;
|
||||
background: var(--indigo-1);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
.loading-indicator .circular-progress {
|
||||
color: var(--lime);
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
border-bottom: 0;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-group--annual-report {
|
||||
.notification-group__icon {
|
||||
color: var(--lime);
|
||||
}
|
||||
|
||||
.notification-group__main .link-button {
|
||||
font-weight: 500;
|
||||
color: var(--lime);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
html {
|
||||
@include base.palette;
|
||||
|
||||
&[data-user-theme='system'] {
|
||||
&:where([data-user-theme='system']) {
|
||||
color-scheme: dark light;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
100
app/javascript/fonts/silkscreen-wrapstodon/OFL.txt
Normal file
100
app/javascript/fonts/silkscreen-wrapstodon/OFL.txt
Normal file
@@ -0,0 +1,100 @@
|
||||
Below you'll find the original License file for the Silkscreen font.
|
||||
The file used on Mastodon is a custom file subset to only include the
|
||||
characters "Wrapstodon 0123456789" using the Font Squirrel Font-face Generator
|
||||
(https://www.fontsquirrel.com/tools/webfont-generator)
|
||||
|
||||
-----------------------------------------------------------
|
||||
|
||||
Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
BIN
app/javascript/images/archetypes/previews/booster.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/booster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
BIN
app/javascript/images/archetypes/previews/lurker.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/lurker.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
BIN
app/javascript/images/archetypes/previews/oracle.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/oracle.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
BIN
app/javascript/images/archetypes/previews/pollster.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/pollster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
BIN
app/javascript/images/archetypes/previews/replier.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/replier.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
BIN
app/javascript/images/archetypes/space_elements.png
Normal file
BIN
app/javascript/images/archetypes/space_elements.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -22,6 +22,8 @@ export function hydrateStore(rawState) {
|
||||
|
||||
dispatch(hydrateCompose());
|
||||
dispatch(hydrateSearch());
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
if (rawState.accounts) {
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import { reinsertAnnualReport, TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report';
|
||||
import api, { getLinks } from 'mastodon/api';
|
||||
import { compareId } from 'mastodon/compare_id';
|
||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import {timelineDelete} from './timelines_typed';
|
||||
import { timelineDelete } from './timelines_typed';
|
||||
|
||||
export { disconnectTimeline } from './timelines_typed';
|
||||
|
||||
@@ -24,9 +25,16 @@ export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
|
||||
export const TIMELINE_INSERT = 'TIMELINE_INSERT';
|
||||
|
||||
// When adding new special markers here, make sure to update TIMELINE_NON_STATUS_MARKERS in actions/timelines_typed.js
|
||||
export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions';
|
||||
export const TIMELINE_GAP = null;
|
||||
|
||||
export const TIMELINE_NON_STATUS_MARKERS = [
|
||||
TIMELINE_GAP,
|
||||
TIMELINE_SUGGESTIONS,
|
||||
TIMELINE_WRAPSTODON,
|
||||
];
|
||||
|
||||
export const loadPending = timeline => ({
|
||||
type: TIMELINE_LOAD_PENDING,
|
||||
timeline,
|
||||
@@ -124,6 +132,7 @@ export function expandTimeline(timelineId, path, params = {}) {
|
||||
|
||||
if (timelineId === 'home') {
|
||||
dispatch(submitMarkers());
|
||||
dispatch(reinsertAnnualReport())
|
||||
}
|
||||
} catch(error) {
|
||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||
|
||||
@@ -2,6 +2,12 @@ import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||
|
||||
import { TIMELINE_NON_STATUS_MARKERS } from './timelines';
|
||||
|
||||
export function isNonStatusId(value: unknown) {
|
||||
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
||||
}
|
||||
|
||||
export const disconnectTimeline = createAction(
|
||||
'timeline/disconnect',
|
||||
({ timeline }: { timeline: string }) => ({
|
||||
|
||||
@@ -117,6 +117,7 @@ class Status extends ImmutablePureComponent {
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
showThread: PropTypes.bool,
|
||||
showActions: PropTypes.bool,
|
||||
isQuotedPost: PropTypes.bool,
|
||||
shouldHighlightOnMount: PropTypes.bool,
|
||||
getScrollPosition: PropTypes.func,
|
||||
@@ -381,7 +382,7 @@ class Status extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, hidden, featured, unfocusable, unread, showThread, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props;
|
||||
const { intl, hidden, featured, unfocusable, unread, showThread, showActions = true, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props;
|
||||
|
||||
let { status, account, ...other } = this.props;
|
||||
|
||||
@@ -618,7 +619,7 @@ class Status extends ImmutablePureComponent {
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isQuotedPost &&
|
||||
{(showActions && !isQuotedPost) &&
|
||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} />
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
export const InterceptStatusClicks: React.FC<{
|
||||
onPreventedClick: (
|
||||
clickedArea: 'account' | 'post',
|
||||
event: React.MouseEvent,
|
||||
) => void;
|
||||
children: React.ReactNode;
|
||||
}> = ({ onPreventedClick, children }) => {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const clickTarget = e.target as Element;
|
||||
const allowedElementsSelector =
|
||||
'.video-player, .audio-player, .media-gallery, .content-warning';
|
||||
const allowedElements = wrapperRef.current?.querySelectorAll(
|
||||
allowedElementsSelector,
|
||||
);
|
||||
const isTargetClickAllowed =
|
||||
allowedElements &&
|
||||
Array.from(allowedElements).some((element) => {
|
||||
return clickTarget === element || element.contains(clickTarget);
|
||||
});
|
||||
|
||||
if (!isTargetClickAllowed) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const wasAccountAreaClicked = !!clickTarget.closest(
|
||||
'a.status__display-name',
|
||||
);
|
||||
|
||||
onPreventedClick(wasAccountAreaClicked ? 'account' : 'post', e);
|
||||
}
|
||||
},
|
||||
[onPreventedClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} onClickCapture={handleClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
@@ -12,7 +14,7 @@ export const AnnualReportAnnouncement: React.FC<{
|
||||
onOpen: () => void;
|
||||
}> = ({ year, hasData, isLoading, onRequestBuild, onOpen }) => {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={classNames('theme-dark', styles.wrapper)}>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='annual_report.announcement.title'
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-on-media);
|
||||
background: var(--color-bg-media-base);
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-primary);
|
||||
background:
|
||||
radial-gradient(at 40% 87%, #240c9a99 0, transparent 50%),
|
||||
radial-gradient(at 19% 10%, #6b0c9a99 0, transparent 50%),
|
||||
radial-gradient(at 90% 27%, #9a0c8299 0, transparent 50%),
|
||||
radial-gradient(at 16% 95%, #1e948299 0, transparent 50%)
|
||||
var(--color-bg-media-base);
|
||||
var(--color-bg-primary);
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
|
||||
h2 {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import {
|
||||
accountFactoryState,
|
||||
annualReportFactory,
|
||||
statusFactoryState,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import { AnnualReport } from '.';
|
||||
|
||||
const SAMPLE_HASHTAG = {
|
||||
name: 'Mastodon',
|
||||
count: 14,
|
||||
};
|
||||
|
||||
const meta = {
|
||||
title: 'Components/AnnualReport',
|
||||
component: AnnualReport,
|
||||
args: {
|
||||
context: 'standalone',
|
||||
},
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState({ display_name: 'Freddie Fruitbat' }),
|
||||
},
|
||||
statuses: {
|
||||
'1': statusFactoryState(),
|
||||
},
|
||||
annualReport: annualReportFactory({
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof AnnualReport>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Standalone: Story = {
|
||||
args: {
|
||||
context: 'standalone',
|
||||
},
|
||||
};
|
||||
|
||||
export const InModal: Story = {
|
||||
args: {
|
||||
context: 'modal',
|
||||
},
|
||||
};
|
||||
|
||||
export const ArchetypeOracle: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'oracle',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'booster',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoNewPosts: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'pollster',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
without_posts: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoNewPostsNoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'replier',
|
||||
without_posts: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,65 +1,214 @@
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import booster from '@/images/archetypes/booster.png';
|
||||
import lurker from '@/images/archetypes/lurker.png';
|
||||
import oracle from '@/images/archetypes/oracle.png';
|
||||
import pollster from '@/images/archetypes/pollster.png';
|
||||
import replier from '@/images/archetypes/replier.png';
|
||||
import type { Archetype as ArchetypeData } from '@/mastodon/models/annual_report';
|
||||
import space_elements from '@/images/archetypes/space_elements.png';
|
||||
import { Avatar } from '@/mastodon/components/avatar';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type {
|
||||
AnnualReport,
|
||||
Archetype as ArchetypeData,
|
||||
} from '@/mastodon/models/annual_report';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { ShareButton } from './share_button';
|
||||
|
||||
export const archetypeNames = defineMessages<ArchetypeData>({
|
||||
booster: {
|
||||
id: 'annual_report.summary.archetype.booster',
|
||||
defaultMessage: 'The cool-hunter',
|
||||
id: 'annual_report.summary.archetype.booster.name',
|
||||
defaultMessage: 'The Archer',
|
||||
},
|
||||
replier: {
|
||||
id: 'annual_report.summary.archetype.replier',
|
||||
defaultMessage: 'The social butterfly',
|
||||
id: 'annual_report.summary.archetype.replier.name',
|
||||
defaultMessage: 'The Butterfly',
|
||||
},
|
||||
pollster: {
|
||||
id: 'annual_report.summary.archetype.pollster',
|
||||
defaultMessage: 'The pollster',
|
||||
id: 'annual_report.summary.archetype.pollster.name',
|
||||
defaultMessage: 'The Wonderer',
|
||||
},
|
||||
lurker: {
|
||||
id: 'annual_report.summary.archetype.lurker',
|
||||
defaultMessage: 'The lurker',
|
||||
id: 'annual_report.summary.archetype.lurker.name',
|
||||
defaultMessage: 'The Stoic',
|
||||
},
|
||||
oracle: {
|
||||
id: 'annual_report.summary.archetype.oracle',
|
||||
defaultMessage: 'The oracle',
|
||||
id: 'annual_report.summary.archetype.oracle.name',
|
||||
defaultMessage: 'The Oracle',
|
||||
},
|
||||
});
|
||||
|
||||
export const Archetype: React.FC<{
|
||||
data: ArchetypeData;
|
||||
}> = ({ data }) => {
|
||||
const intl = useIntl();
|
||||
let illustration;
|
||||
export const archetypeSelfDescriptions = defineMessages<ArchetypeData>({
|
||||
booster: {
|
||||
id: 'annual_report.summary.archetype.booster.desc_self',
|
||||
defaultMessage:
|
||||
'You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.',
|
||||
},
|
||||
replier: {
|
||||
id: 'annual_report.summary.archetype.replier.desc_self',
|
||||
defaultMessage:
|
||||
'You frequently replied to other people’s posts, pollinating Mastodon with new discussions.',
|
||||
},
|
||||
pollster: {
|
||||
id: 'annual_report.summary.archetype.pollster.desc_self',
|
||||
defaultMessage:
|
||||
'You created more polls than other post types, cultivating curiosity on Mastodon.',
|
||||
},
|
||||
lurker: {
|
||||
id: 'annual_report.summary.archetype.lurker.desc_self',
|
||||
defaultMessage:
|
||||
'We know you were out there, somewhere, enjoying Mastodon in your own quiet way.',
|
||||
},
|
||||
oracle: {
|
||||
id: 'annual_report.summary.archetype.oracle.desc_self',
|
||||
defaultMessage:
|
||||
'You created new posts more than replies, keeping Mastodon fresh and future-facing.',
|
||||
},
|
||||
});
|
||||
|
||||
switch (data) {
|
||||
case 'booster':
|
||||
illustration = booster;
|
||||
break;
|
||||
case 'replier':
|
||||
illustration = replier;
|
||||
break;
|
||||
case 'pollster':
|
||||
illustration = pollster;
|
||||
break;
|
||||
case 'lurker':
|
||||
illustration = lurker;
|
||||
break;
|
||||
case 'oracle':
|
||||
illustration = oracle;
|
||||
break;
|
||||
}
|
||||
export const archetypePublicDescriptions = defineMessages<ArchetypeData>({
|
||||
booster: {
|
||||
id: 'annual_report.summary.archetype.booster.desc_public',
|
||||
defaultMessage:
|
||||
'{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.',
|
||||
},
|
||||
replier: {
|
||||
id: 'annual_report.summary.archetype.replier.desc_public',
|
||||
defaultMessage:
|
||||
'{name} frequently replied to other people’s posts, pollinating Mastodon with new discussions.',
|
||||
},
|
||||
pollster: {
|
||||
id: 'annual_report.summary.archetype.pollster.desc_public',
|
||||
defaultMessage:
|
||||
'{name} created more polls than other post types, cultivating curiosity on Mastodon.',
|
||||
},
|
||||
lurker: {
|
||||
id: 'annual_report.summary.archetype.lurker.desc_public',
|
||||
defaultMessage:
|
||||
'We know {name} was out there, somewhere, enjoying Mastodon in their own quiet way.',
|
||||
},
|
||||
oracle: {
|
||||
id: 'annual_report.summary.archetype.oracle.desc_public',
|
||||
defaultMessage:
|
||||
'{name} created new posts more than replies, keeping Mastodon fresh and future-facing.',
|
||||
},
|
||||
});
|
||||
|
||||
const illustrations = {
|
||||
booster,
|
||||
replier,
|
||||
pollster,
|
||||
lurker,
|
||||
oracle,
|
||||
} as const;
|
||||
|
||||
export const Archetype: React.FC<{
|
||||
report: AnnualReport;
|
||||
account?: Account;
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ report, account, context }) => {
|
||||
const intl = useIntl();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const isSelfView = context === 'modal';
|
||||
|
||||
const [isRevealed, setIsRevealed] = useState(!isSelfView);
|
||||
const reveal = useCallback(() => {
|
||||
setIsRevealed(true);
|
||||
wrapperRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const archetype = report.data.archetype;
|
||||
const descriptions = isSelfView
|
||||
? archetypeSelfDescriptions
|
||||
: archetypePublicDescriptions;
|
||||
|
||||
const name = account?.display_name;
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__archetype'>
|
||||
<div className='annual-report__summary__archetype__label'>
|
||||
{intl.formatMessage(archetypeNames[data])}
|
||||
<div
|
||||
className={classNames(styles.box, styles.archetype)}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={0}
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<div className={styles.archetypeArtboard}>
|
||||
{account && (
|
||||
<Avatar
|
||||
account={account}
|
||||
size={50}
|
||||
className={styles.archetypeAvatar}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.archetypeIllustrationWrapper}>
|
||||
<img
|
||||
src={illustrations[archetype]}
|
||||
alt=''
|
||||
className={classNames(
|
||||
styles.archetypeIllustration,
|
||||
isRevealed ? '' : styles.blurredImage,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src={space_elements}
|
||||
alt=''
|
||||
className={styles.archetypePlanetRing}
|
||||
/>
|
||||
</div>
|
||||
<img src={illustration} alt='' />
|
||||
<div className={classNames(styles.content, styles.comfortable)}>
|
||||
<h2 className={styles.title}>
|
||||
{isSelfView ? (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.title_self'
|
||||
defaultMessage='Your archetype'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.title_public'
|
||||
defaultMessage="{name}'s archetype"
|
||||
values={{ name }}
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
<p className={styles.statLarge}>
|
||||
{isRevealed ? (
|
||||
intl.formatMessage(archetypeNames[archetype])
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.die_drei_fragezeichen'
|
||||
defaultMessage='???'
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{isRevealed ? (
|
||||
intl.formatMessage(descriptions[archetype], {
|
||||
name,
|
||||
})
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.reveal_description'
|
||||
defaultMessage='Thanks for being part of Mastodon! Time to find out which archetype you embodied in {year}.'
|
||||
values={{ year: report.year }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{!isRevealed && (
|
||||
<Button onClick={reveal}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.reveal'
|
||||
defaultMessage='Reveal my archetype'
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{isRevealed && isSelfView && <ShareButton report={report} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,68 +1,24 @@
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
|
||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import type { TimeSeriesMonth } from 'mastodon/models/annual_report';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
export const Followers: React.FC<{
|
||||
data: TimeSeriesMonth[];
|
||||
total?: number;
|
||||
}> = ({ data, total }) => {
|
||||
const change = data.reduce((sum, item) => sum + item.followers, 0);
|
||||
|
||||
const cumulativeGraph = data.reduce(
|
||||
(newData, item) => [
|
||||
...newData,
|
||||
item.followers + (newData[newData.length - 1] ?? 0),
|
||||
],
|
||||
[0],
|
||||
);
|
||||
|
||||
count: number;
|
||||
}> = ({ count }) => {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__followers'>
|
||||
<Sparklines data={cumulativeGraph} margin={0}>
|
||||
<svg>
|
||||
<defs>
|
||||
<linearGradient id='gradient' x1='0%' y1='0%' x2='0%' y2='100%'>
|
||||
<stop
|
||||
offset='0%'
|
||||
stopColor='var(--sparkline-gradient-top)'
|
||||
stopOpacity='1'
|
||||
/>
|
||||
<stop
|
||||
offset='100%'
|
||||
stopColor='var(--sparkline-gradient-bottom)'
|
||||
stopOpacity='0'
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div className={classNames(styles.box, styles.followers, styles.content)}>
|
||||
<div className={styles.statLarge}>
|
||||
<FormattedNumber value={count} />
|
||||
</div>
|
||||
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
|
||||
<div className='annual-report__summary__followers__foreground'>
|
||||
<div className='annual-report__summary__followers__number'>
|
||||
{change > -1 ? '+' : '-'}
|
||||
<FormattedNumber value={change} />
|
||||
</div>
|
||||
|
||||
<div className='annual-report__summary__followers__label'>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.followers.followers'
|
||||
defaultMessage='followers'
|
||||
/>
|
||||
</span>
|
||||
<div className='annual-report__summary__followers__footnote'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.followers.total'
|
||||
defaultMessage='{count} total'
|
||||
values={{ count: <ShortNumber value={total ?? 0} /> }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.followers.new_followers'
|
||||
defaultMessage='{count, plural, one {new follower} other {new followers}}'
|
||||
values={{ count }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return,
|
||||
@typescript-eslint/no-explicit-any,
|
||||
@typescript-eslint/no-unsafe-assignment */
|
||||
@typescript-eslint/no-unsafe-assignment,
|
||||
@typescript-eslint/no-unsafe-member-access,
|
||||
@typescript-eslint/no-unsafe-call */
|
||||
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { DisplayName } from '@/mastodon/components/display_name';
|
||||
import { toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { InterceptStatusClicks } from 'mastodon/components/status/intercept_status_clicks';
|
||||
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
|
||||
import type { TopStatuses } from 'mastodon/models/annual_report';
|
||||
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
|
||||
const getPictureInPicture = makeGetPictureInPicture() as unknown as (
|
||||
arg0: any,
|
||||
arg1: any,
|
||||
) => any;
|
||||
|
||||
export const HighlightedPost: React.FC<{
|
||||
data: TopStatuses;
|
||||
}> = ({ data }) => {
|
||||
let statusId, label;
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ data, context }) => {
|
||||
const { by_reblogs, by_favourites, by_replies } = data;
|
||||
|
||||
if (data.by_reblogs) {
|
||||
statusId = data.by_reblogs;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.by_reblogs'
|
||||
defaultMessage='most boosted post'
|
||||
/>
|
||||
);
|
||||
} else if (data.by_favourites) {
|
||||
statusId = data.by_favourites;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.by_favourites'
|
||||
defaultMessage='most favourited post'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
statusId = data.by_replies;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.by_replies'
|
||||
defaultMessage='post with the most replies'
|
||||
/>
|
||||
);
|
||||
}
|
||||
const statusId = by_reblogs || by_favourites || by_replies;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const domain = useAppSelector((state) => state.meta.get('domain'));
|
||||
const status = useAppSelector((state) =>
|
||||
statusId ? getStatus(state, { id: statusId }) : undefined,
|
||||
);
|
||||
const pictureInPicture = useAppSelector((state) =>
|
||||
statusId ? getPictureInPicture(state, { id: statusId }) : undefined,
|
||||
);
|
||||
const account = useAppSelector((state) =>
|
||||
me ? state.accounts.get(me) : undefined,
|
||||
);
|
||||
|
||||
const handleToggleHidden = useCallback(() => {
|
||||
dispatch(toggleStatusSpoilers(statusId));
|
||||
}, [dispatch, statusId]);
|
||||
const handleClick = useCallback<
|
||||
ComponentPropsWithoutRef<typeof InterceptStatusClicks>['onPreventedClick']
|
||||
>(
|
||||
(clickedArea) => {
|
||||
const link: string =
|
||||
clickedArea === 'account'
|
||||
? status.getIn(['account', 'url'])
|
||||
: status.get('url');
|
||||
|
||||
if (context === 'standalone') {
|
||||
window.location.href = link;
|
||||
} else {
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
},
|
||||
[status, context],
|
||||
);
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-boosted-post' />
|
||||
return <div className={classNames(styles.box, styles.mostBoostedPost)} />;
|
||||
}
|
||||
|
||||
let label;
|
||||
if (by_reblogs) {
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.boost_count'
|
||||
defaultMessage='This post was boosted {count, plural, one {once} other {# times}}.'
|
||||
values={{ count: status.get('reblogs_count') }}
|
||||
/>
|
||||
);
|
||||
} else if (by_favourites) {
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.favourite_count'
|
||||
defaultMessage='This post was favorited {count, plural, one {once} other {# times}}.'
|
||||
values={{ count: status.get('favourites_count') }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.reply_count'
|
||||
defaultMessage='This post got {count, plural, one {one reply} other {# replies}}.'
|
||||
values={{ count: status.get('replies_count') }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const displayName = (
|
||||
<span className='display-name'>
|
||||
<strong className='display-name__html'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.possessive'
|
||||
defaultMessage="{name}'s"
|
||||
values={{
|
||||
name: <DisplayName account={account} variant='simple' />,
|
||||
}}
|
||||
/>
|
||||
</strong>
|
||||
<span className='display-name__account'>{label}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-boosted-post'>
|
||||
<DetailedStatus
|
||||
status={status}
|
||||
pictureInPicture={pictureInPicture}
|
||||
domain={domain}
|
||||
onToggleHidden={handleToggleHidden}
|
||||
overrideDisplayName={displayName}
|
||||
/>
|
||||
<div className={classNames(styles.box, styles.mostBoostedPost)}>
|
||||
<div className={styles.content}>
|
||||
<h2 className={styles.title}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.title'
|
||||
defaultMessage='Most popular post'
|
||||
/>
|
||||
</h2>
|
||||
{context === 'modal' && <p>{label}</p>}
|
||||
</div>
|
||||
|
||||
<InterceptStatusClicks onPreventedClick={handleClick}>
|
||||
<StatusQuoteManager showActions={false} id={statusId} />
|
||||
</InterceptStatusClicks>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
334
app/javascript/mastodon/features/annual_report/index.module.scss
Normal file
334
app/javascript/mastodon/features/annual_report/index.module.scss
Normal file
@@ -0,0 +1,334 @@
|
||||
$mobile-breakpoint: 540px;
|
||||
|
||||
@font-face {
|
||||
font-family: silkscreen-wrapstodon;
|
||||
src: url('@/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2')
|
||||
format('woff2');
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.modalWrapper {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary);
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.loading-indicator .circular-progress {
|
||||
color: var(--lime);
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
--default-icon-color: var(--color-bg-primary);
|
||||
--default-bg-color: var(--color-text-primary);
|
||||
--hover-icon-color: var(--color-bg-primary);
|
||||
--hover-bg-color: var(--color-text-primary);
|
||||
--corner-distance: 18px;
|
||||
|
||||
position: absolute;
|
||||
top: var(--corner-distance);
|
||||
right: var(--corner-distance);
|
||||
padding: 8px;
|
||||
border-radius: 100%;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
--corner-distance: 16px;
|
||||
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
--gradient-strength: 0.4;
|
||||
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
max-width: 600px;
|
||||
padding: 24px;
|
||||
padding-top: 40px;
|
||||
contain: layout;
|
||||
flex: 0 0 auto;
|
||||
pointer-events: all;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-primary);
|
||||
background:
|
||||
radial-gradient(
|
||||
at 10% 27%,
|
||||
rgba(83, 12, 154, var(--gradient-strength)) 0,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
at 91% 10%,
|
||||
rgba(30, 24, 223, var(--gradient-strength)) 0,
|
||||
transparent 25%
|
||||
),
|
||||
radial-gradient(
|
||||
at 10% 91%,
|
||||
rgba(22, 218, 228, var(--gradient-strength)) 0,
|
||||
transparent 40%
|
||||
),
|
||||
radial-gradient(
|
||||
at 75% 87%,
|
||||
rgba(37, 31, 217, var(--gradient-strength)) 0,
|
||||
transparent 20%
|
||||
),
|
||||
radial-gradient(
|
||||
at 84% 60%,
|
||||
rgba(95, 30, 148, var(--gradient-strength)) 0,
|
||||
transparent 40%
|
||||
)
|
||||
var(--color-bg-primary);
|
||||
border-radius: 40px;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding-inline: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 18px;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-family: silkscreen-wrapstodon, monospace;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
padding-inline: 40px; // Prevent overlap with close button
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
font-size: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.stack {
|
||||
--grid-spacing: 12px;
|
||||
|
||||
display: grid;
|
||||
gap: var(--grid-spacing);
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
background: rgb(from var(--color-bg-primary) r g b / 60%);
|
||||
box-shadow: inset 0 0 0 1px rgb(from var(--color-text-primary) r g b / 40%);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
white,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
|
||||
&.comfortable {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
color: #c2c8ff;
|
||||
font-weight: 500;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.statLarge {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.statExtraLarge {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.mostBoostedPost {
|
||||
padding: 0;
|
||||
padding-top: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
gap: var(--grid-spacing);
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-areas:
|
||||
'followers hashtag'
|
||||
'new-posts hashtag';
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas:
|
||||
'followers new-posts'
|
||||
'hashtag hashtag';
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.onlyHashtag {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas: 'hashtag';
|
||||
}
|
||||
|
||||
&.noHashtag {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas: 'followers new-posts';
|
||||
}
|
||||
|
||||
&.singleNumber {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-areas: 'number hashtag';
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
grid-template-areas:
|
||||
'number number'
|
||||
'hashtag hashtag';
|
||||
}
|
||||
}
|
||||
|
||||
&.singleNumber.noHashtag {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas: 'number';
|
||||
}
|
||||
}
|
||||
|
||||
.followers {
|
||||
grid-area: followers;
|
||||
|
||||
.singleNumber & {
|
||||
grid-area: number;
|
||||
}
|
||||
}
|
||||
|
||||
.newPosts {
|
||||
grid-area: new-posts;
|
||||
|
||||
.singleNumber & {
|
||||
grid-area: number;
|
||||
}
|
||||
}
|
||||
|
||||
.mostUsedHashtag {
|
||||
grid-area: hashtag;
|
||||
padding-block: 24px;
|
||||
}
|
||||
|
||||
.archetype {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
max-width: 460px;
|
||||
}
|
||||
}
|
||||
|
||||
.archetypeArtboard {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
width: 180px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.archetypeAvatar {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 4px;
|
||||
border-radius: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.archetypeIllustrationWrapper {
|
||||
position: relative;
|
||||
width: 92px;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 100%;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: inset -10px -4px 15px #00000080;
|
||||
}
|
||||
}
|
||||
|
||||
.archetypeIllustration {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.blurredImage {
|
||||
filter: blur(10px);
|
||||
}
|
||||
|
||||
.archetypePlanetRing {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
@@ -1,95 +1,112 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import { focusCompose, resetCompose } from '@/mastodon/actions/compose';
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_report';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { Archetype, archetypeNames } from './archetype';
|
||||
import { Archetype } from './archetype';
|
||||
import { Followers } from './followers';
|
||||
import { HighlightedPost } from './highlighted_post';
|
||||
import styles from './index.module.scss';
|
||||
import { MostUsedHashtag } from './most_used_hashtag';
|
||||
import { NewPosts } from './new_posts';
|
||||
|
||||
const shareMessage = defineMessage({
|
||||
id: 'annual_report.summary.share_message',
|
||||
defaultMessage: 'I got the {archetype} archetype!',
|
||||
});
|
||||
const moduleClassNames = classNames.bind(styles);
|
||||
|
||||
// Share = false when using the embedded version of the report.
|
||||
export const AnnualReport: FC<{ share?: boolean }> = ({ share = true }) => {
|
||||
const currentAccount = useAppSelector((state) =>
|
||||
me ? state.accounts.get(me) : undefined,
|
||||
);
|
||||
export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
context = 'standalone',
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const report = useAppSelector((state) => state.annualReport.report);
|
||||
const account = useAppSelector((state) => {
|
||||
if (me) {
|
||||
return state.accounts.get(me);
|
||||
}
|
||||
if (report?.schema_version === 2) {
|
||||
return state.accounts.get(report.account_id);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const close = useCallback(() => {
|
||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||
}, [dispatch]);
|
||||
|
||||
// Close modal when navigating away from within
|
||||
const { pathname } = useLocation();
|
||||
const [initialPathname] = useState(pathname);
|
||||
useEffect(() => {
|
||||
if (pathname !== initialPathname) {
|
||||
close();
|
||||
}
|
||||
}, [pathname, initialPathname, close]);
|
||||
|
||||
if (!report) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
const newPostCount = report.data.time_series.reduce(
|
||||
(sum, item) => sum + item.statuses,
|
||||
0,
|
||||
);
|
||||
|
||||
const newFollowerCount =
|
||||
context === 'modal' &&
|
||||
report.data.time_series.reduce((sum, item) => sum + item.followers, 0);
|
||||
|
||||
const topHashtag = report.data.top_hashtags[0];
|
||||
|
||||
return (
|
||||
<div className='annual-report'>
|
||||
<div className='annual-report__header'>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.thanks'
|
||||
defaultMessage='Thanks for being part of Mastodon!'
|
||||
<div className={moduleClassNames(styles.wrapper, 'theme-dark')}>
|
||||
<div className={styles.header}>
|
||||
<h1>Wrapstodon {report.year}</h1>
|
||||
{account && <p>@{account.acct}</p>}
|
||||
{context === 'modal' && (
|
||||
<IconButton
|
||||
title={intl.formatMessage({
|
||||
id: 'annual_report.summary.close',
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
className={styles.closeButton}
|
||||
icon='close'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={close}
|
||||
/>
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.here_it_is'
|
||||
defaultMessage='Here is your {year} in review:'
|
||||
values={{ year: report.year }}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='annual-report__bento annual-report__summary'>
|
||||
<Archetype data={report.data.archetype} />
|
||||
<HighlightedPost data={report.data.top_statuses} />
|
||||
<Followers
|
||||
data={report.data.time_series}
|
||||
total={currentAccount?.followers_count}
|
||||
/>
|
||||
<MostUsedHashtag data={report.data.top_hashtags} />
|
||||
<NewPosts data={report.data.time_series} />
|
||||
{share && <ShareButton report={report} />}
|
||||
<div className={styles.stack}>
|
||||
<HighlightedPost data={report.data.top_statuses} context={context} />
|
||||
<div
|
||||
className={moduleClassNames(styles.statsGrid, {
|
||||
noHashtag: !topHashtag,
|
||||
onlyHashtag: !(newFollowerCount && newPostCount),
|
||||
singleNumber: !!newFollowerCount !== !!newPostCount,
|
||||
})}
|
||||
>
|
||||
{!!newFollowerCount && <Followers count={newFollowerCount} />}
|
||||
{!!newPostCount && <NewPosts count={newPostCount} />}
|
||||
{topHashtag && (
|
||||
<MostUsedHashtag
|
||||
hashtag={topHashtag}
|
||||
name={account?.display_name}
|
||||
context={context}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Archetype report={report} account={account} context={context} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const handleShareClick = useCallback(() => {
|
||||
// Generate the share message.
|
||||
const archetypeName = intl.formatMessage(
|
||||
archetypeNames[report.data.archetype],
|
||||
);
|
||||
const shareLines = [
|
||||
intl.formatMessage(shareMessage, {
|
||||
archetype: archetypeName,
|
||||
}),
|
||||
];
|
||||
// Share URL is only available for schema version 2.
|
||||
if (report.schema_version === 2 && report.share_url) {
|
||||
shareLines.push(report.share_url);
|
||||
}
|
||||
shareLines.push(`#Wrapstodon${report.year}`);
|
||||
|
||||
// Reset the composer and focus it with the share message, then close the modal.
|
||||
dispatch(resetCompose());
|
||||
dispatch(focusCompose(shareLines.join('\n\n')));
|
||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||
}, [report, intl, dispatch]);
|
||||
|
||||
return <Button text='Share here' onClick={handleShareClick} />;
|
||||
};
|
||||
|
||||
49
app/javascript/mastodon/features/annual_report/modal.tsx
Normal file
49
app/javascript/mastodon/features/annual_report/modal.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import { AnnualReport } from '.';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const AnnualReportModal: React.FC<{
|
||||
onChangeBackgroundColor: (color: string) => void;
|
||||
}> = ({ onChangeBackgroundColor }) => {
|
||||
useEffect(() => {
|
||||
onChangeBackgroundColor('var(--color-bg-media-base)');
|
||||
}, [onChangeBackgroundColor]);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleCloseModal = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
||||
(e) => {
|
||||
if (e.target === e.currentTarget)
|
||||
dispatch(
|
||||
closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
// It's fine not to provide a keyboard handler here since there is a global
|
||||
// [Esc] key listener that will close open modals.
|
||||
// This onClick handler is needed since the modalWrapper styles overlap the
|
||||
// default modal backdrop, preventing clicks to pass through.
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={classNames(
|
||||
'modal-root__modal',
|
||||
styles.modalWrapper,
|
||||
'theme-dark',
|
||||
)}
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
<AnnualReport context='modal' />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AnnualReportModal;
|
||||
@@ -1,30 +1,46 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { NameAndCount } from 'mastodon/models/annual_report';
|
||||
|
||||
export const MostUsedHashtag: React.FC<{
|
||||
data: NameAndCount[];
|
||||
}> = ({ data }) => {
|
||||
const hashtag = data[0];
|
||||
import styles from './index.module.scss';
|
||||
|
||||
export const MostUsedHashtag: React.FC<{
|
||||
hashtag: NameAndCount;
|
||||
name: string | undefined;
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ hashtag, name, context }) => {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-used-hashtag'>
|
||||
<div className='annual-report__summary__most-used-hashtag__hashtag'>
|
||||
{hashtag ? (
|
||||
<>#{hashtag.name}</>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.none'
|
||||
defaultMessage='None'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='annual-report__summary__most-used-hashtag__label'>
|
||||
<div
|
||||
className={classNames(styles.box, styles.mostUsedHashtag, styles.content)}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.most_used_hashtag'
|
||||
defaultMessage='most used hashtag'
|
||||
defaultMessage='Most used hashtag'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.statExtraLarge}>#{hashtag.name}</div>
|
||||
|
||||
<p>
|
||||
{context === 'modal' ? (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count'
|
||||
defaultMessage='You included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count }}
|
||||
/>
|
||||
) : (
|
||||
name && (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count_public'
|
||||
defaultMessage='{name} included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count, name }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,51 +1,23 @@
|
||||
import { FormattedNumber, FormattedMessage } from 'react-intl';
|
||||
|
||||
import ChatBubbleIcon from '@/material-icons/400-24px/chat_bubble.svg?react';
|
||||
import type { TimeSeriesMonth } from 'mastodon/models/annual_report';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
export const NewPosts: React.FC<{
|
||||
data: TimeSeriesMonth[];
|
||||
}> = ({ data }) => {
|
||||
const posts = data.reduce((sum, item) => sum + item.statuses, 0);
|
||||
|
||||
count: number;
|
||||
}> = ({ count }) => {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__new-posts'>
|
||||
<svg width={500} height={500}>
|
||||
<defs>
|
||||
<pattern
|
||||
id='posts'
|
||||
x='0'
|
||||
y='0'
|
||||
width='32'
|
||||
height='35'
|
||||
patternUnits='userSpaceOnUse'
|
||||
>
|
||||
<circle cx='12' cy='12' r='12' fill='var(--lime)' />
|
||||
<ChatBubbleIcon
|
||||
fill='var(--indigo-1)'
|
||||
x='4'
|
||||
y='4'
|
||||
width='16'
|
||||
height='16'
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect
|
||||
width={500}
|
||||
height={500}
|
||||
fill='url(#posts)'
|
||||
style={{ opacity: 0.2 }}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className='annual-report__summary__new-posts__number'>
|
||||
<FormattedNumber value={posts} />
|
||||
<div className={classNames(styles.box, styles.newPosts, styles.content)}>
|
||||
<div className={styles.statLarge}>
|
||||
<FormattedNumber value={count} />
|
||||
</div>
|
||||
<div className='annual-report__summary__new-posts__label'>
|
||||
|
||||
<div className={styles.title}>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.new_posts.new_posts'
|
||||
defaultMessage='new posts'
|
||||
defaultMessage='{count, plural, one {new post} other {new posts}}'
|
||||
values={{ count }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.wrapper {
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { IconLogo } from '@/mastodon/components/logo';
|
||||
|
||||
import { AnnualReport } from './index';
|
||||
import classes from './share.module.css';
|
||||
|
||||
export const WrapstodonShare: FC = () => {
|
||||
return (
|
||||
<main className={classes.wrapper}>
|
||||
<AnnualReport share={false} />
|
||||
<footer className={classes.footer}>
|
||||
<IconLogo className={classes.logo} />
|
||||
Generated with ♥ by the Mastodon team
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { resetCompose, focusCompose } from '@/mastodon/actions/compose';
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_report';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import { archetypeNames } from './archetype';
|
||||
|
||||
const messages = defineMessages({
|
||||
share_message: {
|
||||
id: 'annual_report.summary.share_message',
|
||||
defaultMessage: 'I got the {archetype} archetype!',
|
||||
},
|
||||
share_on_mastodon: {
|
||||
id: 'annual_report.summary.share_on_mastodon',
|
||||
defaultMessage: 'Share on Mastodon',
|
||||
},
|
||||
});
|
||||
|
||||
export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const handleShareClick = useCallback(() => {
|
||||
// Generate the share message.
|
||||
const archetypeName = intl.formatMessage(
|
||||
archetypeNames[report.data.archetype],
|
||||
);
|
||||
const shareLines = [
|
||||
intl.formatMessage(messages.share_message, {
|
||||
archetype: archetypeName,
|
||||
}),
|
||||
];
|
||||
// Share URL is only available for schema version 2.
|
||||
if (report.schema_version === 2 && report.share_url) {
|
||||
shareLines.push(report.share_url);
|
||||
}
|
||||
shareLines.push(`#Wrapstodon${report.year}`);
|
||||
|
||||
// Reset the composer and focus it with the share message, then close the modal.
|
||||
dispatch(resetCompose());
|
||||
dispatch(focusCompose(shareLines.join('\n\n')));
|
||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||
}, [report, intl, dispatch]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.share_on_mastodon)}
|
||||
onClick={handleShareClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
$mobile-breakpoint: 540px;
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
max-width: 600px;
|
||||
margin-inline: auto;
|
||||
padding: 40px 10px;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding-top: 0;
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2rem;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
a:any-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { IconLogo } from '@/mastodon/components/logo';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
|
||||
import { AnnualReport } from './index';
|
||||
import classes from './shared_page.module.scss';
|
||||
|
||||
export const WrapstodonSharedPage: FC = () => {
|
||||
return (
|
||||
<main className={classes.wrapper}>
|
||||
<AnnualReport />
|
||||
<footer className={classes.footer}>
|
||||
<IconLogo className={classes.logo} />
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.footer'
|
||||
defaultMessage='Generated with {heart} by the Mastodon team'
|
||||
values={{ heart: '♥' }}
|
||||
/>
|
||||
<nav className={classes.nav}>
|
||||
<a href='/about'>
|
||||
<FormattedMessage
|
||||
id='footer.about_this_server'
|
||||
defaultMessage='About'
|
||||
/>
|
||||
</a>
|
||||
{!me && (
|
||||
<a href='https://joinmastodon.org/servers'>
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.sign_up'
|
||||
defaultMessage='Sign up'
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<a href='https://joinmastodon.org/sponsors'>
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.donate'
|
||||
defaultMessage='Donate'
|
||||
/>
|
||||
</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { AnnualReport } from 'mastodon/features/annual_report';
|
||||
|
||||
const AnnualReportModal: React.FC<{
|
||||
onChangeBackgroundColor: (color: string) => void;
|
||||
}> = ({ onChangeBackgroundColor }) => {
|
||||
useEffect(() => {
|
||||
onChangeBackgroundColor('var(--indigo-1)');
|
||||
}, [onChangeBackgroundColor]);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal annual-report-modal'>
|
||||
<AnnualReport />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AnnualReportModal;
|
||||
@@ -4,10 +4,10 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { scrollTopTimeline, loadPending, TIMELINE_SUGGESTIONS } from '@/mastodon/actions/timelines';
|
||||
import { scrollTopTimeline, loadPending } from '@/mastodon/actions/timelines';
|
||||
import { isNonStatusId } from '@/mastodon/actions/timelines_typed';
|
||||
import StatusList from '@/mastodon/components/status_list';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import { TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report';
|
||||
|
||||
const makeGetStatusIds = (pending = false) => createSelector([
|
||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||
@@ -15,7 +15,7 @@ const makeGetStatusIds = (pending = false) => createSelector([
|
||||
(state) => state.get('statuses'),
|
||||
], (columnSettings, statusIds, statuses) => {
|
||||
return statusIds.filter(id => {
|
||||
if (id === null || id === TIMELINE_SUGGESTIONS || id === TIMELINE_WRAPSTODON) return true;
|
||||
if (isNonStatusId(id)) return true;
|
||||
|
||||
const statusForId = statuses.get(id);
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ export function LinkTimeline () {
|
||||
}
|
||||
|
||||
export function AnnualReportModal () {
|
||||
return import('../components/annual_report_modal');
|
||||
return import('../../annual_report/modal');
|
||||
}
|
||||
|
||||
export function ListEdit () {
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
"alt_text_modal.describe_for_people_with_visual_impairments": "Popište to pro osoby se zrakovým postižením…",
|
||||
"alt_text_modal.done": "Hotovo",
|
||||
"announcement.announcement": "Oznámení",
|
||||
"annual_report.announcement.action_build": "Sestavit můj Wrapstodon",
|
||||
"annual_report.announcement.action_view": "Zobrazit můj Wrapstodon",
|
||||
"annual_report.announcement.description": "Zjistěte více o vaší aktivitě na Mastodonu za poslední rok.",
|
||||
"annual_report.announcement.title": "Je zde Wrapstodon {year}",
|
||||
"annual_report.summary.archetype.booster": "Lovec obsahu",
|
||||
"annual_report.summary.archetype.lurker": "Špión",
|
||||
"annual_report.summary.archetype.oracle": "Vědma",
|
||||
@@ -131,6 +135,7 @@
|
||||
"annual_report.summary.new_posts.new_posts": "nové příspěvky",
|
||||
"annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do horních</topLabel><percentage></percentage><bottomLabel> uživatelů domény {domain}.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka, zůstane mezi námi ;).",
|
||||
"annual_report.summary.share_message": "Mám archetyp {archetype}!",
|
||||
"annual_report.summary.thanks": "Děkujeme, že jste součástí Mastodonu!",
|
||||
"attachments_list.unprocessed": "(nezpracováno)",
|
||||
"audio.hide": "Skrýt zvuk",
|
||||
@@ -516,6 +521,7 @@
|
||||
"keyboard_shortcuts.toggle_hidden": "Zobrazit/skrýt text za varováním o obsahu",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Zobrazit/skrýt média",
|
||||
"keyboard_shortcuts.toot": "Začít nový příspěvek",
|
||||
"keyboard_shortcuts.top": "Přesunout na začátek seznamu",
|
||||
"keyboard_shortcuts.translate": "k přeložení příspěvku",
|
||||
"keyboard_shortcuts.unfocus": "Zrušit zaměření na nový příspěvek/hledání",
|
||||
"keyboard_shortcuts.up": "Posunout v seznamu nahoru",
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"account.open_original_page": "Ursprüngliche Seite öffnen",
|
||||
"account.posts": "Beiträge",
|
||||
"account.posts_with_replies": "Beiträge & Antworten",
|
||||
"account.remove_from_followers": "{name} als Follower entfernen",
|
||||
"account.remove_from_followers": "@{name} als Follower entfernen",
|
||||
"account.report": "@{name} melden",
|
||||
"account.requested_follow": "{name} möchte dir folgen",
|
||||
"account.requests_to_follow_you": "Möchte dir folgen",
|
||||
@@ -320,7 +320,7 @@
|
||||
"domain_pill.your_server": "Deine digitale Heimat. Hier „leben“ alle Beiträge von dir. Falls es dir hier nicht gefällt, kannst du jederzeit den Server wechseln und ebenso deine Follower übertragen.",
|
||||
"domain_pill.your_username": "Deine eindeutige Identität auf diesem Server. Es ist möglich, Profile mit dem gleichen Profilnamen auf verschiedenen Servern zu finden.",
|
||||
"dropdown.empty": "Option auswählen",
|
||||
"embed.instructions": "Du kannst diesen Beitrag auf deiner Website einbetten, indem du den nachfolgenden Code kopierst.",
|
||||
"embed.instructions": "Du kannst diesen Beitrag außerhalb des Fediverse (z. B. in deine Website) einbetten, indem du diesen Code kopierst und dort einfügst.",
|
||||
"embed.preview": "Vorschau:",
|
||||
"emoji_button.activity": "Aktivitäten",
|
||||
"emoji_button.clear": "Leeren",
|
||||
@@ -370,8 +370,8 @@
|
||||
"errors.unexpected_crash.copy_stacktrace": "Fehlerdiagnose in die Zwischenablage kopieren",
|
||||
"errors.unexpected_crash.report_issue": "Fehler melden",
|
||||
"explore.suggested_follows": "Profile",
|
||||
"explore.title": "Angesagt",
|
||||
"explore.trending_links": "Neuigkeiten",
|
||||
"explore.title": "Im Trend",
|
||||
"explore.trending_links": "Artikel",
|
||||
"explore.trending_statuses": "Beiträge",
|
||||
"explore.trending_tags": "Hashtags",
|
||||
"featured_carousel.current": "<sr>Beitrag</sr> {current, number}/{max, number}",
|
||||
@@ -414,15 +414,15 @@
|
||||
"follow_suggestions.personalized_suggestion": "Persönliche Empfehlung",
|
||||
"follow_suggestions.popular_suggestion": "Beliebte Empfehlung",
|
||||
"follow_suggestions.popular_suggestion_longer": "Beliebt auf {domain}",
|
||||
"follow_suggestions.similar_to_recently_followed_longer": "Ähnlich zu Profilen, denen du seit kurzem folgst",
|
||||
"follow_suggestions.similar_to_recently_followed_longer": "Ähnelt deinen kürzlich gefolgten Profilen",
|
||||
"follow_suggestions.view_all": "Alle anzeigen",
|
||||
"follow_suggestions.who_to_follow": "Empfohlene Profile",
|
||||
"follow_suggestions.who_to_follow": "Wem folgen?",
|
||||
"followed_tags": "Abonnierte Hashtags",
|
||||
"footer.about": "Über",
|
||||
"footer.about_this_server": "Über",
|
||||
"footer.directory": "Profilverzeichnis",
|
||||
"footer.get_app": "App herunterladen",
|
||||
"footer.keyboard_shortcuts": "Tastenkombinationen",
|
||||
"footer.keyboard_shortcuts": "Tastaturkürzel",
|
||||
"footer.privacy_policy": "Datenschutzerklärung",
|
||||
"footer.source_code": "Quellcode anzeigen",
|
||||
"footer.status": "Status",
|
||||
@@ -483,48 +483,48 @@
|
||||
"interaction_modal.on_another_server": "Auf anderem Server",
|
||||
"interaction_modal.on_this_server": "Auf diesem Server",
|
||||
"interaction_modal.title": "Melde dich an, um fortzufahren",
|
||||
"interaction_modal.username_prompt": "z. B. {example}",
|
||||
"interaction_modal.username_prompt": "Z. B. {example}",
|
||||
"intervals.full.days": "{number, plural, one {# Tag} other {# Tage}}",
|
||||
"intervals.full.hours": "{number, plural, one {# Stunde} other {# Stunden}}",
|
||||
"intervals.full.minutes": "{number, plural, one {# Minute} other {# Minuten}}",
|
||||
"keyboard_shortcuts.back": "Zurücknavigieren",
|
||||
"keyboard_shortcuts.blocked": "Liste blockierter Profile öffnen",
|
||||
"keyboard_shortcuts.blocked": "Blockierte Profile öffnen",
|
||||
"keyboard_shortcuts.boost": "Beitrag teilen",
|
||||
"keyboard_shortcuts.column": "Auf die aktuelle Spalte fokussieren",
|
||||
"keyboard_shortcuts.column": "Aktuelle Spalte fokussieren",
|
||||
"keyboard_shortcuts.compose": "Eingabefeld fokussieren",
|
||||
"keyboard_shortcuts.description": "Beschreibung",
|
||||
"keyboard_shortcuts.direct": "Private Erwähnungen öffnen",
|
||||
"keyboard_shortcuts.down": "Ansicht nach unten bewegen",
|
||||
"keyboard_shortcuts.down": "Auswahl nach unten bewegen",
|
||||
"keyboard_shortcuts.enter": "Beitrag öffnen",
|
||||
"keyboard_shortcuts.favourite": "Beitrag favorisieren",
|
||||
"keyboard_shortcuts.favourites": "Favoriten öffnen",
|
||||
"keyboard_shortcuts.federated": "Föderierte Timeline öffnen",
|
||||
"keyboard_shortcuts.heading": "Tastenkombinationen",
|
||||
"keyboard_shortcuts.heading": "Tastenkürzel",
|
||||
"keyboard_shortcuts.home": "Startseite öffnen",
|
||||
"keyboard_shortcuts.hotkey": "Tastenkürzel",
|
||||
"keyboard_shortcuts.legend": "Tastenkombinationen anzeigen",
|
||||
"keyboard_shortcuts.legend": "Tastenkürzel anzeigen (diese Seite)",
|
||||
"keyboard_shortcuts.load_more": "Schaltfläche „Mehr laden“ fokussieren",
|
||||
"keyboard_shortcuts.local": "Lokale Timeline öffnen",
|
||||
"keyboard_shortcuts.mention": "Profil erwähnen",
|
||||
"keyboard_shortcuts.muted": "Liste stummgeschalteter Profile öffnen",
|
||||
"keyboard_shortcuts.my_profile": "Eigenes Profil aufrufen",
|
||||
"keyboard_shortcuts.notifications": "Benachrichtigungen aufrufen",
|
||||
"keyboard_shortcuts.open_media": "Medieninhalt öffnen",
|
||||
"keyboard_shortcuts.muted": "Stummgeschaltete Profile öffnen",
|
||||
"keyboard_shortcuts.my_profile": "Eigenes Profil öffnen",
|
||||
"keyboard_shortcuts.notifications": "Benachrichtigungen öffnen",
|
||||
"keyboard_shortcuts.open_media": "Medien öffnen",
|
||||
"keyboard_shortcuts.pinned": "Liste angehefteter Beiträge öffnen",
|
||||
"keyboard_shortcuts.profile": "Profil aufrufen",
|
||||
"keyboard_shortcuts.quote": "Beitrag zitieren",
|
||||
"keyboard_shortcuts.reply": "Auf Beitrag antworten",
|
||||
"keyboard_shortcuts.requests": "Liste der Follower-Anfragen aufrufen",
|
||||
"keyboard_shortcuts.search": "Suchleiste fokussieren",
|
||||
"keyboard_shortcuts.spoilers": "Feld für Inhaltswarnung anzeigen/ausblenden",
|
||||
"keyboard_shortcuts.reply": "Beitrag beantworten",
|
||||
"keyboard_shortcuts.requests": "Follower-Anfragen aufrufen",
|
||||
"keyboard_shortcuts.search": "Eingabefeld / Suche fokussieren",
|
||||
"keyboard_shortcuts.spoilers": "Feld für Inhaltswarnung anzeigen / ausblenden",
|
||||
"keyboard_shortcuts.start": "„Auf gehts!“ öffnen",
|
||||
"keyboard_shortcuts.toggle_hidden": "Beitragstext hinter der Inhaltswarnung anzeigen/ausblenden",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Medien anzeigen/ausblenden",
|
||||
"keyboard_shortcuts.toggle_hidden": "Beitrag hinter Inhaltswarnung anzeigen / ausblenden",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Medien anzeigen / ausblenden",
|
||||
"keyboard_shortcuts.toot": "Neuen Beitrag erstellen",
|
||||
"keyboard_shortcuts.top": "Zum Listenanfang springen",
|
||||
"keyboard_shortcuts.translate": "Beitrag übersetzen",
|
||||
"keyboard_shortcuts.unfocus": "Eingabefeld/Suche nicht mehr fokussieren",
|
||||
"keyboard_shortcuts.up": "Ansicht nach oben bewegen",
|
||||
"keyboard_shortcuts.unfocus": "Eingabefeld / Suche nicht mehr fokussieren",
|
||||
"keyboard_shortcuts.up": "Auswahl nach oben bewegen",
|
||||
"learn_more_link.got_it": "Verstanden",
|
||||
"learn_more_link.learn_more": "Mehr erfahren",
|
||||
"lightbox.close": "Schließen",
|
||||
@@ -547,7 +547,7 @@
|
||||
"lists.done": "Fertig",
|
||||
"lists.edit": "Liste bearbeiten",
|
||||
"lists.exclusive": "Mitglieder auf der Startseite ausblenden",
|
||||
"lists.exclusive_hint": "Profile, die sich auf dieser Liste befinden, werden nicht auf deiner Startseite angezeigt, damit deren Beiträge nicht doppelt erscheinen.",
|
||||
"lists.exclusive_hint": "Profile, die sich auf dieser Liste befinden, werden nicht im Feed deiner Startseite angezeigt, damit deren Beiträge nicht doppelt erscheinen.",
|
||||
"lists.find_users_to_add": "Suche nach Profilen, um sie hinzuzufügen",
|
||||
"lists.list_members_count": "{count, plural, one {# Mitglied} other {# Mitglieder}}",
|
||||
"lists.list_name": "Titel der Liste",
|
||||
@@ -556,19 +556,19 @@
|
||||
"lists.no_members_yet": "Keine Mitglieder vorhanden.",
|
||||
"lists.no_results_found": "Keine Suchergebnisse.",
|
||||
"lists.remove_member": "Entfernen",
|
||||
"lists.replies_policy.followed": "Alle folgenden Profile",
|
||||
"lists.replies_policy.followed": "alle folgenden Profile",
|
||||
"lists.replies_policy.list": "Mitglieder der Liste",
|
||||
"lists.replies_policy.none": "Niemanden",
|
||||
"lists.replies_policy.none": "niemanden",
|
||||
"lists.save": "Speichern",
|
||||
"lists.search": "Suchen",
|
||||
"lists.show_replies_to": "Antworten von Listenmitgliedern einbeziehen an …",
|
||||
"load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
|
||||
"loading_indicator.label": "Wird geladen …",
|
||||
"loading_indicator.label": "Lädt …",
|
||||
"media_gallery.hide": "Ausblenden",
|
||||
"moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.",
|
||||
"mute_modal.hide_from_notifications": "Benachrichtigungen ausblenden",
|
||||
"mute_modal.hide_options": "Einstellungen ausblenden",
|
||||
"mute_modal.indefinite": "Bis ich die Stummschaltung aufhebe",
|
||||
"mute_modal.hide_from_notifications": "Auch aus den Benachrichtigungen entfernen",
|
||||
"mute_modal.hide_options": "Optionen ausblenden",
|
||||
"mute_modal.indefinite": "Dauerhaft, bis ich die Stummschaltung aufhebe",
|
||||
"mute_modal.show_options": "Optionen anzeigen",
|
||||
"mute_modal.they_can_mention_and_follow": "Das Profil wird dich weiterhin erwähnen und dir folgen können, aber du wirst davon nichts sehen.",
|
||||
"mute_modal.they_wont_know": "Das Profil wird nicht erkennen können, dass du es stummgeschaltet hast.",
|
||||
@@ -578,7 +578,7 @@
|
||||
"navigation_bar.about": "Über",
|
||||
"navigation_bar.account_settings": "Passwort und Sicherheit",
|
||||
"navigation_bar.administration": "Administration",
|
||||
"navigation_bar.advanced_interface": "Im erweiterten Webinterface öffnen",
|
||||
"navigation_bar.advanced_interface": "Erweitertes Webinterface öffnen",
|
||||
"navigation_bar.automated_deletion": "Automatisiertes Löschen",
|
||||
"navigation_bar.blocks": "Blockierte Profile",
|
||||
"navigation_bar.bookmarks": "Lesezeichen",
|
||||
@@ -588,8 +588,8 @@
|
||||
"navigation_bar.filters": "Stummgeschaltete Wörter",
|
||||
"navigation_bar.follow_requests": "Follower-Anfragen",
|
||||
"navigation_bar.followed_tags": "Abonnierte Hashtags",
|
||||
"navigation_bar.follows_and_followers": "Follower und Folge ich",
|
||||
"navigation_bar.import_export": "Importieren und exportieren",
|
||||
"navigation_bar.follows_and_followers": "Follower & Folge ich",
|
||||
"navigation_bar.import_export": "Importieren & exportieren",
|
||||
"navigation_bar.lists": "Listen",
|
||||
"navigation_bar.live_feed_local": "Live-Feed (Dieser Server)",
|
||||
"navigation_bar.live_feed_public": "Live-Feed (Alle Server)",
|
||||
@@ -599,9 +599,9 @@
|
||||
"navigation_bar.mutes": "Stummgeschaltete Profile",
|
||||
"navigation_bar.opened_in_classic_interface": "Beiträge, Konten und andere bestimmte Seiten werden standardmäßig im klassischen Webinterface geöffnet.",
|
||||
"navigation_bar.preferences": "Einstellungen",
|
||||
"navigation_bar.privacy_and_reach": "Datenschutz und Reichweite",
|
||||
"navigation_bar.privacy_and_reach": "Datenschutz & Reichweite",
|
||||
"navigation_bar.search": "Suche",
|
||||
"navigation_bar.search_trends": "Suche / Angesagt",
|
||||
"navigation_bar.search_trends": "Suche / Trends",
|
||||
"navigation_panel.collapse_followed_tags": "Menü für abonnierte Hashtags schließen",
|
||||
"navigation_panel.collapse_lists": "Listen-Menü schließen",
|
||||
"navigation_panel.expand_followed_tags": "Menü für abonnierte Hashtags öffnen",
|
||||
@@ -635,7 +635,7 @@
|
||||
"notification.moderation_warning": "Du wurdest von den Moderator*innen verwarnt",
|
||||
"notification.moderation_warning.action_delete_statuses": "Einige deiner Beiträge sind entfernt worden.",
|
||||
"notification.moderation_warning.action_disable": "Dein Konto wurde deaktiviert.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Einige deiner Beiträge wurden mit einer Inhaltswarnung versehen.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Einige deiner Beiträge haben eine Inhaltswarnung erhalten.",
|
||||
"notification.moderation_warning.action_none": "Dein Konto ist von den Moderator*innen verwarnt worden.",
|
||||
"notification.moderation_warning.action_sensitive": "Deine zukünftigen Beiträge werden mit einer Inhaltswarnung versehen.",
|
||||
"notification.moderation_warning.action_silence": "Dein Konto wurde eingeschränkt.",
|
||||
@@ -650,7 +650,7 @@
|
||||
"notification.relationships_severance_event.domain_block": "Ein Admin von {from} hat {target} blockiert – darunter {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst.",
|
||||
"notification.relationships_severance_event.learn_more": "Mehr erfahren",
|
||||
"notification.relationships_severance_event.user_domain_block": "Du hast {target} blockiert – {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst, wurden entfernt.",
|
||||
"notification.status": "{name} postete …",
|
||||
"notification.status": "{name} veröffentlichte …",
|
||||
"notification.update": "{name} bearbeitete einen Beitrag",
|
||||
"notification_requests.accept": "Akzeptieren",
|
||||
"notification_requests.accept_multiple": "{count, plural, one {# Anfrage akzeptieren …} other {# Anfragen akzeptieren …}}",
|
||||
@@ -686,9 +686,9 @@
|
||||
"notifications.column_settings.mention": "Erwähnungen:",
|
||||
"notifications.column_settings.poll": "Umfrageergebnisse:",
|
||||
"notifications.column_settings.push": "Push-Benachrichtigungen",
|
||||
"notifications.column_settings.quote": "Zitate:",
|
||||
"notifications.column_settings.quote": "Zitierte Beiträge:",
|
||||
"notifications.column_settings.reblog": "Geteilte Beiträge:",
|
||||
"notifications.column_settings.show": "In dieser Spalte anzeigen",
|
||||
"notifications.column_settings.show": "Im Feed „Benachrichtigungen“ anzeigen",
|
||||
"notifications.column_settings.sound": "Ton abspielen",
|
||||
"notifications.column_settings.status": "Neue Beiträge:",
|
||||
"notifications.column_settings.unread_notifications.category": "Ungelesene Benachrichtigungen",
|
||||
@@ -697,10 +697,10 @@
|
||||
"notifications.filter.all": "Alles",
|
||||
"notifications.filter.boosts": "Geteilte Beiträge",
|
||||
"notifications.filter.favourites": "Favoriten",
|
||||
"notifications.filter.follows": "Folgt",
|
||||
"notifications.filter.follows": "Neue Follower",
|
||||
"notifications.filter.mentions": "Erwähnungen",
|
||||
"notifications.filter.polls": "Umfrageergebnisse",
|
||||
"notifications.filter.statuses": "Neue Beiträge von Profilen, denen du folgst",
|
||||
"notifications.filter.statuses": "Neue Beiträge von abonnierten Profilen",
|
||||
"notifications.grant_permission": "Berechtigung erteilen.",
|
||||
"notifications.group": "{count} Benachrichtigungen",
|
||||
"notifications.mark_as_read": "Alle Benachrichtigungen als gelesen markieren",
|
||||
@@ -710,18 +710,18 @@
|
||||
"notifications.policy.accept": "Akzeptieren",
|
||||
"notifications.policy.accept_hint": "In Benachrichtigungen anzeigen",
|
||||
"notifications.policy.drop": "Ignorieren",
|
||||
"notifications.policy.drop_hint": "In die Leere senden und nie wieder sehen",
|
||||
"notifications.policy.drop_hint": "Ins Nirwana befördern und auf Nimmerwiedersehen!",
|
||||
"notifications.policy.filter": "Filtern",
|
||||
"notifications.policy.filter_hint": "An gefilterte Benachrichtigungen im Posteingang senden",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Durch Server-Moderator*innen eingeschränkt",
|
||||
"notifications.policy.filter_limited_accounts_title": "moderierten Konten",
|
||||
"notifications.policy.filter_new_accounts.hint": "Innerhalb {days, plural, one {des letzten Tages} other {der letzten # Tagen}} erstellt",
|
||||
"notifications.policy.filter_new_accounts_title": "neuen Konten",
|
||||
"notifications.policy.filter_not_followers_hint": "Einschließlich Profilen, die dir seit weniger als {days, plural, one {einem Tag} other {# Tagen}} folgen",
|
||||
"notifications.policy.filter_hint": "Im separaten Feed „Gefilterte Benachrichtigungen“ anzeigen",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Durch Server-Moderator*innen eingeschränkte Profile",
|
||||
"notifications.policy.filter_limited_accounts_title": "eingeschränkten Konten",
|
||||
"notifications.policy.filter_new_accounts.hint": "Konto {days, plural, one {seit gestern} other {in den vergangenen # Tagen}} registriert",
|
||||
"notifications.policy.filter_new_accounts_title": "neuen Profilen",
|
||||
"notifications.policy.filter_not_followers_hint": "Einschließlich Profilen, die mir seit weniger als {days, plural, one {einem Tag} other {# Tagen}} folgen",
|
||||
"notifications.policy.filter_not_followers_title": "Profilen, die mir nicht folgen",
|
||||
"notifications.policy.filter_not_following_hint": "Bis du sie manuell genehmigst",
|
||||
"notifications.policy.filter_not_following_hint": "… bis ich sie manuell genehmige",
|
||||
"notifications.policy.filter_not_following_title": "Profilen, denen ich nicht folge",
|
||||
"notifications.policy.filter_private_mentions_hint": "Solange sie keine Antwort auf deine Erwähnung ist oder du dem Profil nicht folgst",
|
||||
"notifications.policy.filter_private_mentions_hint": "… solange sie keine Antwort auf meine Erwähnungen sind – oder ich den Profilen nicht folge",
|
||||
"notifications.policy.filter_private_mentions_title": "unerwünschten privaten Erwähnungen",
|
||||
"notifications.policy.title": "Benachrichtigungen verwalten von …",
|
||||
"notifications_permission_banner.enable": "Aktiviere Desktop-Benachrichtigungen",
|
||||
@@ -763,17 +763,17 @@
|
||||
"privacy.public.long": "Alle innerhalb und außerhalb von Mastodon",
|
||||
"privacy.public.short": "Öffentlich",
|
||||
"privacy.quote.anyone": "{visibility} – alle dürfen zitieren",
|
||||
"privacy.quote.disabled": "{visibility} – niemand darf zitieren",
|
||||
"privacy.quote.limited": "{visibility} – eingeschränktes Zitieren",
|
||||
"privacy.quote.disabled": "{visibility} – Zitieren deaktiviert",
|
||||
"privacy.quote.limited": "{visibility} – nur Follower",
|
||||
"privacy.unlisted.additional": "Das Verhalten ist wie bei „Öffentlich“, jedoch gibt es einige Einschränkungen. Der Beitrag wird nicht in „Live-Feeds“, „Erkunden“, Hashtags oder über die Mastodon-Suchfunktion auffindbar sein – selbst wenn die zugehörige Einstellung aktiviert wurde.",
|
||||
"privacy.unlisted.long": "Verborgen vor Suchergebnissen, Trends und öffentlichen Timelines",
|
||||
"privacy.unlisted.long": "Verborgen vor Suchen, Trends und öffentlichen Timelines",
|
||||
"privacy.unlisted.short": "Öffentlich (still)",
|
||||
"privacy_policy.last_updated": "Stand: {date}",
|
||||
"privacy_policy.title": "Datenschutzerklärung",
|
||||
"quote_error.edit": "Beim Bearbeiten eines Beitrags können keine Zitate hinzugefügt werden.",
|
||||
"quote_error.poll": "Zitieren ist bei Umfragen nicht gestattet.",
|
||||
"quote_error.private_mentions": "Das Zitieren ist bei privaten Erwähnungen nicht erlaubt.",
|
||||
"quote_error.quote": "Es ist jeweils nur ein Zitat zulässig.",
|
||||
"quote_error.edit": "Beim Bearbeiten eines vorhandenen Beitrags können keine Zitate hinzugefügt werden.",
|
||||
"quote_error.poll": "Zitieren ist bei Umfragen nicht erlaubt.",
|
||||
"quote_error.private_mentions": "Zitieren ist bei privaten Erwähnungen nicht erlaubt.",
|
||||
"quote_error.quote": "Es darf nur ein Beitrag zitiert werden.",
|
||||
"quote_error.unauthorized": "Du bist nicht berechtigt, diesen Beitrag zu zitieren.",
|
||||
"quote_error.upload": "Zitieren ist mit Medien-Anhängen nicht möglich.",
|
||||
"recommended": "Empfohlen",
|
||||
@@ -783,7 +783,7 @@
|
||||
"relative_time.days": "{number} T.",
|
||||
"relative_time.full.days": "vor {number, plural, one {# Tag} other {# Tagen}}",
|
||||
"relative_time.full.hours": "vor {number, plural, one {# Stunde} other {# Stunden}}",
|
||||
"relative_time.full.just_now": "gerade eben",
|
||||
"relative_time.full.just_now": "soeben",
|
||||
"relative_time.full.minutes": "vor {number, plural, one {# Minute} other {# Minuten}}",
|
||||
"relative_time.full.seconds": "vor {number, plural, one {1 Sekunde} other {# Sekunden}}",
|
||||
"relative_time.hours": "{number} Std.",
|
||||
@@ -793,13 +793,13 @@
|
||||
"relative_time.today": "heute",
|
||||
"remove_quote_hint.button_label": "Verstanden",
|
||||
"remove_quote_hint.message": "Klicke dafür im Beitrag auf „{icon} Mehr“.",
|
||||
"remove_quote_hint.title": "Möchtest du aus dem zitierten Beitrag entfernt werden?",
|
||||
"remove_quote_hint.title": "Deinen zitierten Beitrag aus diesem Beitrag entfernen?",
|
||||
"reply_indicator.attachments": "{count, plural, one {# Anhang} other {# Anhänge}}",
|
||||
"reply_indicator.cancel": "Abbrechen",
|
||||
"reply_indicator.poll": "Umfrage",
|
||||
"report.block": "Blockieren",
|
||||
"report.block_explanation": "Du wirst keine Beiträge mehr von diesem Konto sehen. Das blockierte Konto wird deine Beiträge nicht mehr sehen oder dir folgen können. Die Person könnte mitbekommen, dass du sie blockiert hast.",
|
||||
"report.categories.legal": "Rechtlich",
|
||||
"report.categories.legal": "Rechtliches",
|
||||
"report.categories.other": "Andere",
|
||||
"report.categories.spam": "Spam",
|
||||
"report.categories.violation": "Der Inhalt verletzt eine oder mehrere Serverregeln",
|
||||
@@ -808,9 +808,9 @@
|
||||
"report.category.title_account": "Profil",
|
||||
"report.category.title_status": "Beitrag",
|
||||
"report.close": "Fertig",
|
||||
"report.comment.title": "Gibt es etwas anderes, was wir wissen sollten?",
|
||||
"report.forward": "Meldung zusätzlich an {target} weiterleiten",
|
||||
"report.forward_hint": "Dieses Konto gehört zu einem anderen Server. Soll eine anonymisierte Kopie der Meldung auch dorthin gesendet werden?",
|
||||
"report.comment.title": "Gibt es noch etwas, das wir wissen sollten?",
|
||||
"report.forward": "Meldung auch an den externen Server {target} weiterleiten",
|
||||
"report.forward_hint": "Das gemeldete Konto befindet sich auf einem anderen Server. Soll zusätzlich eine anonymisierte Kopie deiner Meldung an diesen Server geschickt werden?",
|
||||
"report.mute": "Stummschalten",
|
||||
"report.mute_explanation": "Du wirst keine Beiträge mehr von diesem Konto sehen. Das stummgeschaltete Konto wird dir weiterhin folgen und deine Beiträge sehen können. Die Person wird nicht mitbekommen, dass du sie stummgeschaltet hast.",
|
||||
"report.next": "Weiter",
|
||||
@@ -826,9 +826,9 @@
|
||||
"report.reasons.violation": "Das verstößt gegen Serverregeln",
|
||||
"report.reasons.violation_description": "Du bist dir sicher, dass eine bestimmte Regel gebrochen wurde",
|
||||
"report.rules.subtitle": "Wähle alle zutreffenden Inhalte aus",
|
||||
"report.rules.title": "Welche Regeln werden verletzt?",
|
||||
"report.rules.title": "Gegen welche Regeln wurde verstoßen?",
|
||||
"report.statuses.subtitle": "Wähle alle zutreffenden Inhalte aus",
|
||||
"report.statuses.title": "Gibt es Beiträge, die diese Meldung bekräftigen?",
|
||||
"report.statuses.title": "Gibt es Beiträge, die diese Meldung stützen?",
|
||||
"report.submit": "Senden",
|
||||
"report.target": "{target} melden",
|
||||
"report.thanks.take_action": "Das sind deine Möglichkeiten zu bestimmen, was du auf Mastodon sehen möchtest:",
|
||||
@@ -845,7 +845,7 @@
|
||||
"report_notification.categories.spam": "Spam",
|
||||
"report_notification.categories.spam_sentence": "Spam",
|
||||
"report_notification.categories.violation": "Regelverstoß",
|
||||
"report_notification.categories.violation_sentence": "Regelverletzung",
|
||||
"report_notification.categories.violation_sentence": "Verstoß gegen die Serverregeln",
|
||||
"report_notification.open": "Meldung öffnen",
|
||||
"search.clear": "Suchanfrage löschen",
|
||||
"search.no_recent_searches": "Keine früheren Suchanfragen",
|
||||
@@ -885,13 +885,13 @@
|
||||
"status.admin_account": "@{name} moderieren",
|
||||
"status.admin_domain": "{domain} moderieren",
|
||||
"status.admin_status": "Beitrag moderieren",
|
||||
"status.all_disabled": "Teilen und Zitieren von Beiträgen ist deaktiviert",
|
||||
"status.all_disabled": "Teilen und Zitieren sind deaktiviert",
|
||||
"status.block": "@{name} blockieren",
|
||||
"status.bookmark": "Lesezeichen setzen",
|
||||
"status.cancel_reblog_private": "Beitrag nicht mehr teilen",
|
||||
"status.cannot_quote": "Beitrag kann nicht zitiert werden",
|
||||
"status.cannot_quote": "Diesen Beitrag darfst du nicht zitieren",
|
||||
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
|
||||
"status.contains_quote": "Enthält Zitat",
|
||||
"status.contains_quote": "Enthält zitierten Beitrag",
|
||||
"status.context.loading": "Weitere Antworten laden",
|
||||
"status.context.loading_error": "Weitere Antworten konnten nicht geladen werden",
|
||||
"status.context.loading_success": "Neue Antworten geladen",
|
||||
@@ -907,10 +907,10 @@
|
||||
"status.direct_indicator": "Private Erwähnung",
|
||||
"status.edit": "Beitrag bearbeiten",
|
||||
"status.edited": "Zuletzt am {date} bearbeitet",
|
||||
"status.edited_x_times": "{count, plural, one {{count}-mal} other {{count}-mal}} bearbeitet",
|
||||
"status.edited_x_times": "{count, plural, one {{count} ×} other {{count} ×}} bearbeitet",
|
||||
"status.embed": "Code zum Einbetten",
|
||||
"status.favourite": "Favorisieren",
|
||||
"status.favourites_count": "{count, plural, one {{counter} Mal favorisiert} other {{counter} Mal favorisiert}}",
|
||||
"status.favourites_count": "{count, plural, one {{counter} × favorisiert} other {{counter} × favorisiert}}",
|
||||
"status.filter": "Beitrag filtern",
|
||||
"status.history.created": "{name} erstellte {date}",
|
||||
"status.history.edited": "{name} bearbeitete {date}",
|
||||
@@ -945,14 +945,14 @@
|
||||
"status.quotes.empty": "Diesen Beitrag hat bisher noch niemand zitiert. Sobald es jemand tut, wird das Profil hier erscheinen.",
|
||||
"status.quotes.local_other_disclaimer": "Durch Autor*in abgelehnte Zitate werden nicht angezeigt.",
|
||||
"status.quotes.remote_other_disclaimer": "Nur Zitate von {domain} werden hier garantiert angezeigt. Durch Autor*in abgelehnte Zitate werden nicht angezeigt.",
|
||||
"status.quotes_count": "{count, plural, one {{counter} Mal zitiert} other {{counter} Mal zitiert}}",
|
||||
"status.quotes_count": "{count, plural, one {{counter} × zitiert} other {{counter} × zitiert}}",
|
||||
"status.read_more": "Gesamten Beitrag anschauen",
|
||||
"status.reblog": "Teilen",
|
||||
"status.reblog_or_quote": "Teilen oder zitieren",
|
||||
"status.reblog_private": "Erneut mit deinen Followern teilen",
|
||||
"status.reblogged_by": "{name} teilte",
|
||||
"status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier erscheinen.",
|
||||
"status.reblogs_count": "{count, plural, one {{counter} Mal geteilt} other {{counter} Mal geteilt}}",
|
||||
"status.reblogs_count": "{count, plural, one {{counter} × geteilt} other {{counter} × geteilt}}",
|
||||
"status.redraft": "Löschen und neu erstellen",
|
||||
"status.remove_bookmark": "Lesezeichen entfernen",
|
||||
"status.remove_favourite": "Aus Favoriten entfernen",
|
||||
@@ -1036,9 +1036,9 @@
|
||||
"visibility_modal.helper.unlisted_quoting": "Sollten dich andere zitieren, werden ihre zitierten Beiträge ebenfalls nicht in den Trends und öffentlichen Timelines angezeigt.",
|
||||
"visibility_modal.instructions": "Lege fest, wer mit diesem Beitrag interagieren darf. Du hast auch die Möglichkeit, diese Einstellung auf alle zukünftigen Beiträge anzuwenden. Gehe zu: <link>Einstellungen > Standardeinstellungen für Beiträge</link>",
|
||||
"visibility_modal.privacy_label": "Sichtbarkeit",
|
||||
"visibility_modal.quote_followers": "Nur Follower",
|
||||
"visibility_modal.quote_followers": "Nur meine Follower dürfen mich zitieren",
|
||||
"visibility_modal.quote_label": "Wer darf mich zitieren?",
|
||||
"visibility_modal.quote_nobody": "Nur ich selbst",
|
||||
"visibility_modal.quote_public": "Alle",
|
||||
"visibility_modal.quote_nobody": "Niemand darf mich zitieren",
|
||||
"visibility_modal.quote_public": "Alle dürfen mich zitieren",
|
||||
"visibility_modal.save": "Speichern"
|
||||
}
|
||||
|
||||
@@ -117,26 +117,44 @@
|
||||
"annual_report.announcement.action_view": "View my Wrapstodon",
|
||||
"annual_report.announcement.description": "Discover more about your engagement on Mastodon over the past year.",
|
||||
"annual_report.announcement.title": "Wrapstodon {year} has arrived",
|
||||
"annual_report.summary.archetype.booster": "The cool-hunter",
|
||||
"annual_report.summary.archetype.lurker": "The lurker",
|
||||
"annual_report.summary.archetype.oracle": "The oracle",
|
||||
"annual_report.summary.archetype.pollster": "The pollster",
|
||||
"annual_report.summary.archetype.replier": "The social butterfly",
|
||||
"annual_report.summary.followers.followers": "followers",
|
||||
"annual_report.summary.followers.total": "{count} total",
|
||||
"annual_report.summary.here_it_is": "Here is your {year} in review:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "most favourited post",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "most boosted post",
|
||||
"annual_report.summary.highlighted_post.by_replies": "post with the most replies",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}'s",
|
||||
"annual_report.shared_page.donate": "Donate",
|
||||
"annual_report.shared_page.footer": "Generated with {heart} by the Mastodon team",
|
||||
"annual_report.shared_page.sign_up": "Sign up",
|
||||
"annual_report.summary.archetype.booster.desc_public": "{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.",
|
||||
"annual_report.summary.archetype.booster.desc_self": "You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.",
|
||||
"annual_report.summary.archetype.booster.name": "The Archer",
|
||||
"annual_report.summary.archetype.die_drei_fragezeichen": "???",
|
||||
"annual_report.summary.archetype.lurker.desc_public": "We know {name} was out there, somewhere, enjoying Mastodon in their own quiet way.",
|
||||
"annual_report.summary.archetype.lurker.desc_self": "We know you were out there, somewhere, enjoying Mastodon in your own quiet way.",
|
||||
"annual_report.summary.archetype.lurker.name": "The Stoic",
|
||||
"annual_report.summary.archetype.oracle.desc_public": "{name} created new posts more than replies, keeping Mastodon fresh and future-facing.",
|
||||
"annual_report.summary.archetype.oracle.desc_self": "You created new posts more than replies, keeping Mastodon fresh and future-facing.",
|
||||
"annual_report.summary.archetype.oracle.name": "The Oracle",
|
||||
"annual_report.summary.archetype.pollster.desc_public": "{name} created more polls than other post types, cultivating curiosity on Mastodon.",
|
||||
"annual_report.summary.archetype.pollster.desc_self": "You created more polls than other post types, cultivating curiosity on Mastodon.",
|
||||
"annual_report.summary.archetype.pollster.name": "The Wonderer",
|
||||
"annual_report.summary.archetype.replier.desc_public": "{name} frequently replied to other people’s posts, pollinating Mastodon with new discussions.",
|
||||
"annual_report.summary.archetype.replier.desc_self": "You frequently replied to other people’s posts, pollinating Mastodon with new discussions.",
|
||||
"annual_report.summary.archetype.replier.name": "The Butterfly",
|
||||
"annual_report.summary.archetype.reveal": "Reveal my archetype",
|
||||
"annual_report.summary.archetype.reveal_description": "Thanks for being part of Mastodon! Time to find out which archetype you embodied in {year}.",
|
||||
"annual_report.summary.archetype.title_public": "{name}'s archetype",
|
||||
"annual_report.summary.archetype.title_self": "Your archetype",
|
||||
"annual_report.summary.close": "Close",
|
||||
"annual_report.summary.followers.new_followers": "{count, plural, one {new follower} other {new followers}}",
|
||||
"annual_report.summary.highlighted_post.boost_count": "This post was boosted {count, plural, one {once} other {# times}}.",
|
||||
"annual_report.summary.highlighted_post.favourite_count": "This post was favorited {count, plural, one {once} other {# times}}.",
|
||||
"annual_report.summary.highlighted_post.reply_count": "This post got {count, plural, one {one reply} other {# replies}}.",
|
||||
"annual_report.summary.highlighted_post.title": "Most popular post",
|
||||
"annual_report.summary.most_used_app.most_used_app": "most used app",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag",
|
||||
"annual_report.summary.most_used_hashtag.none": "None",
|
||||
"annual_report.summary.most_used_hashtag.used_count": "You included this hashtag in {count, plural, one {one post} other {# posts}}.",
|
||||
"annual_report.summary.most_used_hashtag.used_count_public": "{name} included this hashtag in {count, plural, one {one post} other {# posts}}.",
|
||||
"annual_report.summary.new_posts.new_posts": "new posts",
|
||||
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of {domain} users.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.",
|
||||
"annual_report.summary.share_message": "I got the {archetype} archetype!",
|
||||
"annual_report.summary.thanks": "Thanks for being part of Mastodon!",
|
||||
"annual_report.summary.share_on_mastodon": "Share on Mastodon",
|
||||
"attachments_list.unprocessed": "(unprocessed)",
|
||||
"audio.hide": "Hide audio",
|
||||
"block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"about.contact": "Kontakt:",
|
||||
"about.default_locale": "Vaikimisi",
|
||||
"about.disclaimer": "Mastodon on tasuta ja vaba tarkvara ning Mastodon gGmbH kaubamärk.",
|
||||
"about.domain_blocks.no_reason_available": "Põhjus teadmata",
|
||||
"about.domain_blocks.no_reason_available": "Põhjus on teadmata",
|
||||
"about.domain_blocks.preamble": "Mastodon lubab tavaliselt vaadata sisu ning suhelda kasutajatega ükskõik millisest teisest fediversumi serverist. Need on erandid, mis on paika pandud sellel kindlal serveril.",
|
||||
"about.domain_blocks.silenced.explanation": "Sa ei näe üldiselt profiile ja sisu sellelt serverilt, kui sa just tahtlikult seda ei otsi või jälgimise moel nõusolekut ei anna.",
|
||||
"about.domain_blocks.silenced.title": "Piiratud",
|
||||
@@ -113,6 +113,10 @@
|
||||
"alt_text_modal.describe_for_people_with_visual_impairments": "Kirjelda seda nägemispuudega inimeste jaoks…",
|
||||
"alt_text_modal.done": "Valmis",
|
||||
"announcement.announcement": "Teadaanne",
|
||||
"annual_report.announcement.action_build": "Koosta kokkuvõte minu tegevusest Mastodonis",
|
||||
"annual_report.announcement.action_view": "Vaata kokkuvõtet minu tegevusest Mastodonis",
|
||||
"annual_report.announcement.description": "Vaata teavet oma suhestumise kohta Mastodonis eelmisel aastal.",
|
||||
"annual_report.announcement.title": "{year}. aasta Mastodoni kokkuvõte on valmis",
|
||||
"annual_report.summary.archetype.booster": "Ägesisu küttija",
|
||||
"annual_report.summary.archetype.lurker": "Hiilija",
|
||||
"annual_report.summary.archetype.oracle": "Oraakel",
|
||||
@@ -131,6 +135,7 @@
|
||||
"annual_report.summary.new_posts.new_posts": "uus postitus",
|
||||
"annual_report.summary.percentile.text": "<topLabel>See paneb su top</topLabel><percentage></percentage><bottomLabel> {domain} kasutajate hulka.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "Vägev.",
|
||||
"annual_report.summary.share_message": "Minu arhetüüp on {archetype}!",
|
||||
"annual_report.summary.thanks": "Tänud olemast osa Mastodonist!",
|
||||
"attachments_list.unprocessed": "(töötlemata)",
|
||||
"audio.hide": "Peida audio",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"about.default_locale": "پیشگزیده",
|
||||
"about.disclaimer": "ماستودون نرمافزار آزاد و نشان تجاری یک شرکت غیر انتفاعی با مسئولیت محدود آلمانی است.",
|
||||
"about.domain_blocks.no_reason_available": "دلیلی موجود نیست",
|
||||
"about.domain_blocks.preamble": "ماستودون عموماً میگذارد محتوا را از از هر کارساز دیگری در دنیای شبکههای اجتماعی غیرمتمرکز دیده و با آنان برهمکنش داشته باشید. اینها استثناهایی هستند که روی این کارساز خاص وضع شدهاند.",
|
||||
"about.domain_blocks.preamble": "ماستودون عموماً میگذارد محتوا را از هر کارساز دیگری در دنیای شبکههای اجتماعی غیرمتمرکز دیده و با آنان برهمکنش داشته باشید. اینها استثناهایی هستند که روی این کارساز خاص وضع شدهاند.",
|
||||
"about.domain_blocks.silenced.explanation": "عموماً نمایهها و محتوا از این کارساز را نمیبینید، مگر این که به طور خاص دنبالشان گشته یا با پی گیری، داوطلب دیدنشان شوید.",
|
||||
"about.domain_blocks.silenced.title": "محدود",
|
||||
"about.domain_blocks.suspended.explanation": "هیچ دادهای از این کارساز پردازش، ذخیره یا مبادله نخواهد شد، که هرگونه برهمکنش یا ارتباط با کاربران این کارساز را غیرممکن خواهد کرد.",
|
||||
@@ -121,7 +121,7 @@
|
||||
"annual_report.summary.archetype.lurker": "کمپیدا",
|
||||
"annual_report.summary.archetype.oracle": "غیبگو",
|
||||
"annual_report.summary.archetype.pollster": "نظرسنج",
|
||||
"annual_report.summary.archetype.replier": "پاسخگو",
|
||||
"annual_report.summary.archetype.replier": "پاسخگو",
|
||||
"annual_report.summary.followers.followers": "دنبال کننده",
|
||||
"annual_report.summary.followers.total": "در مجموع {count}",
|
||||
"annual_report.summary.here_it_is": "بازبینی {year} تان:",
|
||||
@@ -135,6 +135,7 @@
|
||||
"annual_report.summary.new_posts.new_posts": "فرستهٔ جدید",
|
||||
"annual_report.summary.percentile.text": "<topLabel>بین کاربران {domain} جزو</topLabel><percentage></percentage><bottomLabel>برتر هستید.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "به برنی خبر نمیدهیم.",
|
||||
"annual_report.summary.share_message": "من کهنالگوی {archetype} را گرفتم!",
|
||||
"annual_report.summary.thanks": "سپاس که بخشی از ماستودون هستید!",
|
||||
"attachments_list.unprocessed": "(پردازش نشده)",
|
||||
"audio.hide": "نهفتن صدا",
|
||||
|
||||
@@ -364,7 +364,7 @@
|
||||
"error.no_hashtag_feed_access": "Liity tai kirjaudu sisään, niin voit tarkastella ja seurata tätä aihetunnistetta.",
|
||||
"error.unexpected_crash.explanation": "Sivua ei voida näyttää oikein ohjelmointivirheen tai selaimen yhteensopivuusvajeen vuoksi.",
|
||||
"error.unexpected_crash.explanation_addons": "Sivua ei voitu näyttää oikein. Tämä virhe johtuu todennäköisesti selaimen lisäosasta tai automaattisista käännöstyökaluista.",
|
||||
"error.unexpected_crash.next_steps": "Kokeile päivittää sivu. Jos se ei auta, voi Mastodonin käyttö ehkä onnistua eri selaimella tai natiivisovelluksella.",
|
||||
"error.unexpected_crash.next_steps": "Kokeile päivittää sivu. Jos se ei auta, Mastodonin käyttö voi ehkä onnistua eri selaimella tai natiivisovelluksella.",
|
||||
"error.unexpected_crash.next_steps_addons": "Yritä poistaa ne käytöstä, ja virkistä sitten sivunlataus. Mikäli ongelma jatkuu, voit mahdollisesti käyttää Mastodonia eri selaimella tai natiivilla sovelluksella.",
|
||||
"errors.unexpected_crash.copy_stacktrace": "Kopioi pinon jäljitys leikepöydälle",
|
||||
"errors.unexpected_crash.report_issue": "Ilmoita ongelmasta",
|
||||
@@ -867,7 +867,7 @@
|
||||
"search_results.all": "Kaikki",
|
||||
"search_results.hashtags": "Aihetunnisteet",
|
||||
"search_results.no_results": "Ei tuloksia.",
|
||||
"search_results.no_search_yet": "Koeta hakea julkaisuja, profiileja tai aihetunnisteita.",
|
||||
"search_results.no_search_yet": "Kokeile hakea julkaisuja, profiileja tai aihetunnisteita.",
|
||||
"search_results.see_all": "Näytä kaikki",
|
||||
"search_results.statuses": "Julkaisut",
|
||||
"search_results.title": "Haku ”{q}”",
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
"annual_report.summary.new_posts.new_posts": "nýggir postar",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Tað fær teg í topp</topLabel><percentage></percentage><bottomLabel>av {domain} brúkarum.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "Vit fara ikki at fortelja Bernie tað.",
|
||||
"annual_report.summary.share_message": "Eg fekk {archetype} frumsniðið!",
|
||||
"annual_report.summary.thanks": "Takk fyri at tú er partur av Mastodon!",
|
||||
"attachments_list.unprocessed": "(óviðgjørt)",
|
||||
"audio.hide": "Fjal ljóð",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"account.familiar_followers_two": "{name1} ir {name2} seka",
|
||||
"account.featured": "Rodomi",
|
||||
"account.featured.accounts": "Profiliai",
|
||||
"account.featured.hashtags": "Saitažodžiai",
|
||||
"account.featured.hashtags": "Grotažymės",
|
||||
"account.featured_tags.last_status_at": "Paskutinis įrašas {date}",
|
||||
"account.featured_tags.last_status_never": "Nėra įrašų",
|
||||
"account.follow": "Sekti",
|
||||
@@ -130,7 +130,7 @@
|
||||
"annual_report.summary.highlighted_post.by_replies": "įrašas su daugiausiai atsakymų",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}",
|
||||
"annual_report.summary.most_used_app.most_used_app": "labiausiai naudota programa",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "labiausiai naudotas saitažodis",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "labiausiai naudota grotažymė",
|
||||
"annual_report.summary.most_used_hashtag.none": "Nieko",
|
||||
"annual_report.summary.new_posts.new_posts": "nauji įrašai",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Tai reiškia, kad esate tarp</topLabel><percentage></percentage><bottomLabel>populiariausių {domain} naudotojų.</bottomLabel>",
|
||||
@@ -209,7 +209,7 @@
|
||||
"compose.saved.body": "Įrašas išsaugotas.",
|
||||
"compose_form.direct_message_warning_learn_more": "Sužinoti daugiau",
|
||||
"compose_form.encryption_warning": "„Mastodon“ įrašai nėra visapusiškai šifruojami. Per „Mastodon“ nesidalyk jokia slapta informacija.",
|
||||
"compose_form.hashtag_warning": "Šis įrašas nebus įtrauktas į jokį saitažodį, nes ji nėra vieša. Tik viešų įrašų galima ieškoti pagal saitažodį.",
|
||||
"compose_form.hashtag_warning": "Šis įrašas nebus įtrauktas į jokį grotažodį, nes jis nėra viešas. Tik vieši įrašai gali būti ieškomi pagal grotažymę.",
|
||||
"compose_form.lock_disclaimer": "Tavo paskyra nėra {locked}. Bet kas gali sekti tave ir peržiūrėti tik sekėjams skirtus įrašus.",
|
||||
"compose_form.lock_disclaimer.lock": "užrakinta",
|
||||
"compose_form.placeholder": "Kas tavo mintyse?",
|
||||
@@ -337,15 +337,15 @@
|
||||
"emoji_button.search_results": "Paieškos rezultatai",
|
||||
"emoji_button.symbols": "Simboliai",
|
||||
"emoji_button.travel": "Kelionės ir vietos",
|
||||
"empty_column.account_featured.me": "Jūs dar nieko neparyškinote. Ar žinojote, kad savo profilyje galite parodyti dažniausiai naudojamas žymes ir netgi savo draugų paskyras?",
|
||||
"empty_column.account_featured.other": "{acct} dar nieko neparyškino. Ar žinojote, kad savo profilyje galite pateikti dažniausiai naudojamus žymes ir netgi savo draugų paskyras?",
|
||||
"empty_column.account_featured.me": "Jūs dar nieko neparyškinote. Ar žinojote, kad savo profilyje galite parodyti dažniausiai naudojamas grotažymes ir netgi savo draugų paskyras?",
|
||||
"empty_column.account_featured.other": "{acct} dar nieko neparyškino. Ar žinojote, kad savo profilyje galite pateikti dažniausiai naudojamus grotžymes ir netgi savo draugų paskyras?",
|
||||
"empty_column.account_featured_other.unknown": "Ši paskyra dar nieko neparodė.",
|
||||
"empty_column.account_hides_collections": "Šis (-i) naudotojas (-a) pasirinko nepadaryti šią informaciją prieinamą.",
|
||||
"empty_column.account_suspended": "Paskyra pristabdyta.",
|
||||
"empty_column.account_timeline": "Nėra čia įrašų.",
|
||||
"empty_column.account_unavailable": "Profilis neprieinamas.",
|
||||
"empty_column.blocks": "Dar neužblokavai nė vieno naudotojo.",
|
||||
"empty_column.bookmarked_statuses": "Dar neturi nė vienos įrašo pridėtos žymės. Kai vieną iš jų pridėsi į žymes, jis bus rodomas čia.",
|
||||
"empty_column.bookmarked_statuses": "Dar neturi nė vienos įrašo su žyma. Kai vieną žymų pridėsi prie įrašo, jis bus rodomas čia.",
|
||||
"empty_column.community": "Vietinė laiko skalė yra tuščia. Parašyk ką nors viešai, kad pradėtum sąveikauti.",
|
||||
"empty_column.direct": "Dar neturi jokių privačių paminėjimų. Kai išsiųsi arba gausi vieną iš jų, jis bus rodomas čia.",
|
||||
"empty_column.disabled_feed": "Šis srautas buvo išjungtas jūsų serverio administratorių.",
|
||||
@@ -354,8 +354,8 @@
|
||||
"empty_column.favourited_statuses": "Dar neturi mėgstamų įrašų. Kai vieną iš jų pamėgsi, jis bus rodomas čia.",
|
||||
"empty_column.favourites": "Šio įrašo dar niekas nepamėgo. Kai kas nors tai padarys, jie bus rodomi čia.",
|
||||
"empty_column.follow_requests": "Dar neturi jokių sekimo prašymų. Kai gausi tokį prašymą, jis bus rodomas čia.",
|
||||
"empty_column.followed_tags": "Dar neseki jokių saitažodžių. Kai tai padarysi, jie bus rodomi čia.",
|
||||
"empty_column.hashtag": "Nėra nieko šiame saitažodyje kol kas.",
|
||||
"empty_column.followed_tags": "Dar neseki jokių grotažymių. Kai tai padarysi, jos bus rodomos čia.",
|
||||
"empty_column.hashtag": "Šioje gratažymėje kol kas nieko nėra.",
|
||||
"empty_column.home": "Tavo pagrindinio laiko skalė tuščia. Sek daugiau žmonių, kad ją užpildytum.",
|
||||
"empty_column.list": "Šiame sąraše dar nieko nėra. Kai šio sąrašo nariai paskelbs naujus įrašus, jie bus rodomi čia.",
|
||||
"empty_column.mutes": "Dar nesi nutildęs (-usi) nė vieno naudotojo.",
|
||||
@@ -373,7 +373,7 @@
|
||||
"explore.title": "Populiaru",
|
||||
"explore.trending_links": "Naujienos",
|
||||
"explore.trending_statuses": "Įrašai",
|
||||
"explore.trending_tags": "Saitažodžiai",
|
||||
"explore.trending_tags": "Grotažymės",
|
||||
"featured_carousel.current": "<sr>Įrašas</sr> {current, number} / {max, number}",
|
||||
"featured_carousel.header": "{count, plural, one {Iškeltas įrašas} few {Iškelti įrašai} many {Iškeltų įrašų} other {Iškelti įrašai}}",
|
||||
"featured_carousel.slide": "Įrašas {current, number} iš {max, number}",
|
||||
@@ -417,7 +417,7 @@
|
||||
"follow_suggestions.similar_to_recently_followed_longer": "Panašūs į profilius, kuriuos neseniai seki",
|
||||
"follow_suggestions.view_all": "Peržiūrėti viską",
|
||||
"follow_suggestions.who_to_follow": "Ką sekti",
|
||||
"followed_tags": "Sekami saitažodžiai",
|
||||
"followed_tags": "Sekamos grotažymės",
|
||||
"footer.about": "Apie",
|
||||
"footer.about_this_server": "Apie",
|
||||
"footer.directory": "Profilių katalogas",
|
||||
@@ -429,26 +429,26 @@
|
||||
"footer.terms_of_service": "Paslaugų sąlygos",
|
||||
"generic.saved": "Išsaugota",
|
||||
"getting_started.heading": "Kaip pradėti",
|
||||
"hashtag.admin_moderation": "Atverti prižiūrėjimo sąsają saitažodžiui #{name}",
|
||||
"hashtag.admin_moderation": "Atverti moderavimo langą #{name}",
|
||||
"hashtag.browse": "Naršyti įrašus su #{hashtag}",
|
||||
"hashtag.browse_from_account": "Naršyti @{name} įrašus su žyma #{hashtag}",
|
||||
"hashtag.browse_from_account": "Naršyti @{name} įrašus su grotažyme #{hashtag}",
|
||||
"hashtag.column_header.tag_mode.all": "ir {additional}",
|
||||
"hashtag.column_header.tag_mode.any": "ar {additional}",
|
||||
"hashtag.column_header.tag_mode.none": "be {additional}",
|
||||
"hashtag.column_settings.select.no_options_message": "Pasiūlymų nerasta.",
|
||||
"hashtag.column_settings.select.placeholder": "Įvesti saitažodžius…",
|
||||
"hashtag.column_settings.select.placeholder": "Įvesk grotažymes…",
|
||||
"hashtag.column_settings.tag_mode.all": "Visi šie",
|
||||
"hashtag.column_settings.tag_mode.any": "Bet kuris iš šių",
|
||||
"hashtag.column_settings.tag_mode.none": "Nė vienas iš šių",
|
||||
"hashtag.column_settings.tag_toggle": "Įtraukti papildomas šio stulpelio žymes",
|
||||
"hashtag.counter_by_accounts": "{count, plural, one {{counter} dalyvis} few {{counter} dalyviai} many {{counter} dalyvio} other {{counter} dalyvių}}",
|
||||
"hashtag.column_settings.tag_mode.any": "Bet kuris šių",
|
||||
"hashtag.column_settings.tag_mode.none": "Nei vienas šių",
|
||||
"hashtag.column_settings.tag_toggle": "Įtraukti papildomas žymas į šį stulpelį",
|
||||
"hashtag.counter_by_accounts": "{count, plural, one {{counter} dalyvis} few {{counter} dalyviai} many {{counter} dalyvių} other {{counter} dalyviai}}",
|
||||
"hashtag.counter_by_uses": "{count, plural, one {{counter} įrašas} few {{counter} įrašai} many {{counter} įrašo} other {{counter} įrašų}}",
|
||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} įrašas} few {{counter} įrašai} many {{counter} įrašo} other {{counter} įrašų}} šiandien",
|
||||
"hashtag.feature": "Rodyti profilyje",
|
||||
"hashtag.follow": "Sekti saitažodį",
|
||||
"hashtag.follow": "Sekti grotažymę",
|
||||
"hashtag.mute": "Nutildyti žymą #{hashtag}",
|
||||
"hashtag.unfeature": "Neberodyti profilyje",
|
||||
"hashtag.unfollow": "Nebesekti saitažodį",
|
||||
"hashtag.unfollow": "Nebesekti grotažymės",
|
||||
"hashtags.and_other": "…ir {count, plural, one {# daugiau} few {# daugiau} many {# daugiau}other {# daugiau}}",
|
||||
"hints.profiles.followers_may_be_missing": "Sekėjai šiai profiliui gali būti nepateikti.",
|
||||
"hints.profiles.follows_may_be_missing": "Sekimai šiai profiliui gali būti nepateikti.",
|
||||
@@ -489,7 +489,7 @@
|
||||
"intervals.full.minutes": "{number, plural, one {# minutė} few {# minutes} many {# minutės} other {# minučių}}",
|
||||
"keyboard_shortcuts.back": "Naršyti atgal",
|
||||
"keyboard_shortcuts.blocked": "Atidaryti užblokuotų naudotojų sąrašą",
|
||||
"keyboard_shortcuts.boost": "Pakelti įrašą",
|
||||
"keyboard_shortcuts.boost": "Dalintis įrašu",
|
||||
"keyboard_shortcuts.column": "Fokusuoti stulpelį",
|
||||
"keyboard_shortcuts.compose": "Fokusuoti rengykles teksto sritį",
|
||||
"keyboard_shortcuts.description": "Aprašymas",
|
||||
@@ -587,7 +587,7 @@
|
||||
"navigation_bar.favourites": "Mėgstami",
|
||||
"navigation_bar.filters": "Nutildyti žodžiai",
|
||||
"navigation_bar.follow_requests": "Sekimo prašymai",
|
||||
"navigation_bar.followed_tags": "Sekami saitažodžiai",
|
||||
"navigation_bar.followed_tags": "Sekamos grotažymės",
|
||||
"navigation_bar.follows_and_followers": "Sekimai ir sekėjai",
|
||||
"navigation_bar.import_export": "Importas ir eksportas",
|
||||
"navigation_bar.lists": "Sąrašai",
|
||||
@@ -604,7 +604,7 @@
|
||||
"navigation_bar.search_trends": "Paieška / Populiaru",
|
||||
"navigation_panel.collapse_followed_tags": "Sutraukti sekamų žymių meniu",
|
||||
"navigation_panel.collapse_lists": "Sutraukti sąrašo meniu",
|
||||
"navigation_panel.expand_followed_tags": "Išskleisti sekamų žymių meniu",
|
||||
"navigation_panel.expand_followed_tags": "Išskleisti sekamų grotažymių meniu",
|
||||
"navigation_panel.expand_lists": "Išskleisti sąrašo meniu",
|
||||
"not_signed_in_indicator.not_signed_in": "Norint pasiekti šį išteklį, reikia prisijungti.",
|
||||
"notification.admin.report": "{name} pranešė {target}",
|
||||
@@ -635,16 +635,16 @@
|
||||
"notification.moderation_warning": "Gavai prižiūrėjimo įspėjimą",
|
||||
"notification.moderation_warning.action_delete_statuses": "Kai kurie tavo įrašai buvo pašalintos.",
|
||||
"notification.moderation_warning.action_disable": "Tavo paskyra buvo išjungta.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Kai kurie tavo įrašai buvo pažymėtos kaip jautrios.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Kai kurie tavo įrašai buvo pažymėti kaip turintys jautrią informaciją.",
|
||||
"notification.moderation_warning.action_none": "Tavo paskyra gavo prižiūrėjimo įspėjimą.",
|
||||
"notification.moderation_warning.action_sensitive": "Nuo šiol tavo įrašai bus pažymėti kaip jautrūs.",
|
||||
"notification.moderation_warning.action_sensitive": "Nuo šiol tavo įrašai bus pažymėti kaip su jautria informacija.",
|
||||
"notification.moderation_warning.action_silence": "Tavo paskyra buvo apribota.",
|
||||
"notification.moderation_warning.action_suspend": "Tavo paskyra buvo sustabdyta.",
|
||||
"notification.own_poll": "Tavo apklausa baigėsi",
|
||||
"notification.poll": "Baigėsi apklausa, kurioje balsavai",
|
||||
"notification.quoted_update": "{name} redagavo jūsų cituotą įrašą",
|
||||
"notification.reblog": "{name} pakėlė tavo įrašą",
|
||||
"notification.reblog.name_and_others_with_link": "{name} ir <a>{count, plural,one {dar kažkas} few {# kiti} other {# kitų}}</a> paryškino tavo įrašą",
|
||||
"notification.reblog": "{name} dalinosi tavo įrašu",
|
||||
"notification.reblog.name_and_others_with_link": "{name} ir <a>{count, plural,one {dar kažkas} few {# kiti} other {# kitų}}</a> pasidalino tavo įrašu",
|
||||
"notification.relationships_severance_event": "Prarasti sąryšiai su {name}",
|
||||
"notification.relationships_severance_event.account_suspension": "Administratorius iš {from} sustabdė {target}, todėl jūs nebegalėsite gauti jų naujienų ar bendrauti su jais.",
|
||||
"notification.relationships_severance_event.domain_block": "Administratorius iš {from} užblokavo {target}, įskaitant {followersCount} iš tavo sekėjų ir {followingCount, plural,one {# žmogų, kurį} few {# žmones, kuriuos} other {# žmonių, kuriuos}} seki.",
|
||||
@@ -687,7 +687,7 @@
|
||||
"notifications.column_settings.poll": "Balsavimo rezultatai:",
|
||||
"notifications.column_settings.push": "Tiesioginiai pranešimai",
|
||||
"notifications.column_settings.quote": "Paminėjimai:",
|
||||
"notifications.column_settings.reblog": "Pakėlimai:",
|
||||
"notifications.column_settings.reblog": "Pasidalinimai:",
|
||||
"notifications.column_settings.show": "Rodyti stulpelyje",
|
||||
"notifications.column_settings.sound": "Paleisti garsą",
|
||||
"notifications.column_settings.status": "Nauji įrašai:",
|
||||
@@ -695,7 +695,7 @@
|
||||
"notifications.column_settings.unread_notifications.highlight": "Paryškinti neperskaitytus pranešimus",
|
||||
"notifications.column_settings.update": "Redagavimai:",
|
||||
"notifications.filter.all": "Visi",
|
||||
"notifications.filter.boosts": "Pakėlimai",
|
||||
"notifications.filter.boosts": "Pasidalinimai",
|
||||
"notifications.filter.favourites": "Mėgstami",
|
||||
"notifications.filter.follows": "Sekimai",
|
||||
"notifications.filter.mentions": "Paminėjimai",
|
||||
@@ -765,7 +765,7 @@
|
||||
"privacy.quote.anyone": "{visibility}, kiekvienas gali cituoti",
|
||||
"privacy.quote.disabled": "{visibility}, paminėjimai išjungti",
|
||||
"privacy.quote.limited": "{visibility}, paminėjimai apriboti",
|
||||
"privacy.unlisted.additional": "Tai veikia lygiai taip pat, kaip ir vieša, tik įrašas nebus rodomas tiesioginiuose srautuose, saitažodžiose, naršyme ar Mastodon paieškoje, net jei esi įtraukęs (-usi) visą paskyrą.",
|
||||
"privacy.unlisted.additional": "Tai veikia lygiai taip pat, kaip ir vieša, tik įrašas nebus rodomas tiesioginiuose srautuose, grotažymėse, naršyme ar Mastodon paieškoje, net jei esi įtraukęs (-usi) visą paskyrą.",
|
||||
"privacy.unlisted.long": "Paslėptas nuo „Mastodon“ paieškos rezultatų, tendencijų ir viešų įrašų sienų",
|
||||
"privacy.unlisted.short": "Tyliai vieša",
|
||||
"privacy_policy.last_updated": "Paskutinį kartą atnaujinta {date}",
|
||||
@@ -866,9 +866,9 @@
|
||||
"search_popout.user": "naudotojas",
|
||||
"search_results.accounts": "Profiliai",
|
||||
"search_results.all": "Visi",
|
||||
"search_results.hashtags": "Saitažodžiai",
|
||||
"search_results.hashtags": "Grotažymės",
|
||||
"search_results.no_results": "Nėra rezultatų.",
|
||||
"search_results.no_search_yet": "Pabandykite ieškoti įrašų, profilių arba saitažodžių.",
|
||||
"search_results.no_search_yet": "Pabandykite ieškoti įrašų, profilių arba grotažymių.",
|
||||
"search_results.see_all": "Žiūrėti viską",
|
||||
"search_results.statuses": "Įrašai",
|
||||
"search_results.title": "Paieška užklausai „{q}“",
|
||||
@@ -885,10 +885,10 @@
|
||||
"status.admin_account": "Atidaryti prižiūrėjimo sąsają @{name}",
|
||||
"status.admin_domain": "Atidaryti prižiūrėjimo sąsają {domain}",
|
||||
"status.admin_status": "Atidaryti šį įrašą prižiūrėjimo sąsajoje",
|
||||
"status.all_disabled": "Įrašo pakėlimai ir paminėjimai išjungti",
|
||||
"status.all_disabled": "Įrašo dalinimaisi ir paminėjimai išjungti",
|
||||
"status.block": "Blokuoti @{name}",
|
||||
"status.bookmark": "Pridėti į žymės",
|
||||
"status.cancel_reblog_private": "Nebepasidalinti",
|
||||
"status.bookmark": "Žymė",
|
||||
"status.cancel_reblog_private": "Nesidalinti",
|
||||
"status.cannot_quote": "Jums neleidžiama paminėti šio įrašo",
|
||||
"status.cannot_reblog": "Šis įrašas negali būti pakeltas.",
|
||||
"status.contains_quote": "Turi citatą",
|
||||
@@ -910,6 +910,7 @@
|
||||
"status.edited_x_times": "Redaguota {count, plural, one {{count} kartą} few {{count} kartus} many {{count} karto} other {{count} kartų}}",
|
||||
"status.embed": "Gaukite įterpimo kodą",
|
||||
"status.favourite": "Pamėgti",
|
||||
"status.favourites_count": "{count, plural, one {{counter} patiko} few {{counter} patiko} many {{counter} patiko} other {{counter} patiko}}",
|
||||
"status.filter": "Filtruoti šį įrašą",
|
||||
"status.history.created": "{name} sukurta {date}",
|
||||
"status.history.edited": "{name} redaguota {date}",
|
||||
@@ -944,11 +945,14 @@
|
||||
"status.quotes.empty": "Šio įrašo dar niekas nepaminėjo. Kai kas nors tai padarys, jie bus rodomi čia.",
|
||||
"status.quotes.local_other_disclaimer": "Autoriaus atmesti įrašo paminėjimai nebus rodomi.",
|
||||
"status.quotes.remote_other_disclaimer": "Čia bus rodoma tik paminėjimai iš {domain}. Autoriaus atmesti įrašo paminėjimai nebus rodomi.",
|
||||
"status.quotes_count": "{count, plural, one {{counter} paminėjimas} few {{counter} paminėjimai} many {{counter} paminėjimai} other {{counter} paminėjimai}}",
|
||||
"status.read_more": "Skaityti daugiau",
|
||||
"status.reblog": "Pakelti",
|
||||
"status.reblog_or_quote": "Paryškinti arba cituoti",
|
||||
"status.reblogged_by": "{name} pakėlė",
|
||||
"status.reblogs.empty": "Šio įrašo dar niekas nepakėlė. Kai kas nors tai padarys, jie bus rodomi čia.",
|
||||
"status.reblog": "Dalintis",
|
||||
"status.reblog_or_quote": "Dalintis arba cituoti",
|
||||
"status.reblog_private": "Vėl pasidalinkite su savo sekėjais",
|
||||
"status.reblogged_by": "{name} pasidalino",
|
||||
"status.reblogs.empty": "Šiuo įrašu dar niekas nesidalino. Kai kas nors tai padarys, jie bus rodomi čia.",
|
||||
"status.reblogs_count": "{count, plural, one {{counter} pasidalinimas} few {{counter} pasidalinimai} many {{counter} pasidalinimų} other {{counter} pasidalinimai}}",
|
||||
"status.redraft": "Ištrinti ir parengti iš naujo",
|
||||
"status.remove_bookmark": "Pašalinti žymę",
|
||||
"status.remove_favourite": "Šalinti iš mėgstamų",
|
||||
@@ -975,6 +979,7 @@
|
||||
"subscribed_languages.save": "Išsaugoti pakeitimus",
|
||||
"subscribed_languages.target": "Keisti prenumeruojamas kalbas {target}",
|
||||
"tabs_bar.home": "Pagrindinis",
|
||||
"tabs_bar.menu": "Meniu",
|
||||
"tabs_bar.notifications": "Pranešimai",
|
||||
"tabs_bar.publish": "Naujas įrašas",
|
||||
"tabs_bar.search": "Paieška",
|
||||
@@ -1023,10 +1028,13 @@
|
||||
"visibility_modal.button_title": "Nustatyti matomumą",
|
||||
"visibility_modal.direct_quote_warning.text": "Jei išsaugosite dabartinius nustatymus, įterpta citata bus konvertuota į nuorodą.",
|
||||
"visibility_modal.direct_quote_warning.title": "Cituojami įrašai negali būti įterpiami į privačius paminėjimus",
|
||||
"visibility_modal.header": "Matomumas ir sąveika",
|
||||
"visibility_modal.helper.direct_quoting": "Privatūs paminėjimai, parašyti platformoje „Mastodon“, negali būti cituojami kitų.",
|
||||
"visibility_modal.helper.privacy_editing": "Matomumo nustatymai negali būti keičiami po to, kai įrašas yra paskelbtas.",
|
||||
"visibility_modal.helper.privacy_private_self_quote": "Privačių įrašų paminėjimai negali būti skelbiami viešai.",
|
||||
"visibility_modal.helper.private_quoting": "Tik sekėjams skirti įrašai, parašyti platformoje „Mastodon“, negali būti cituojami kitų.",
|
||||
"visibility_modal.helper.unlisted_quoting": "Kai žmonės jus cituos, jų įrašai taip pat bus paslėpti iš populiariausių naujienų srauto.",
|
||||
"visibility_modal.instructions": "Kontroliuokite, kas gali bendrauti su šiuo įrašu. Taip pat galite taikyti nustatymus visiems būsimiems įrašams, pereidami į <link>Preferences > Posting defaults</link>.",
|
||||
"visibility_modal.privacy_label": "Matomumas",
|
||||
"visibility_modal.quote_followers": "Tik sekėjai",
|
||||
"visibility_modal.quote_label": "Kas gali cituoti",
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
"alt_text_modal.describe_for_people_with_visual_impairments": "請替看有困難ê敘述tsit ê內容…",
|
||||
"alt_text_modal.done": "做好ah",
|
||||
"announcement.announcement": "公告",
|
||||
"annual_report.announcement.action_build": "建立我ê Wrapstodon",
|
||||
"annual_report.announcement.action_view": "看我ê Wrapstodon",
|
||||
"annual_report.announcement.description": "發現其他關係lí佇最近tsi̍t年參與Mastodon ê狀況。",
|
||||
"annual_report.announcement.title": "Wrapstodon {year} 已經kàu ah",
|
||||
"annual_report.summary.archetype.booster": "追求趣味ê",
|
||||
"annual_report.summary.archetype.lurker": "有讀無PO ê",
|
||||
"annual_report.summary.archetype.oracle": "先知",
|
||||
@@ -131,6 +135,7 @@
|
||||
"annual_report.summary.new_posts.new_posts": "新ê PO文",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Tse 予lí變做 {domain} ê用戶ê </topLabel><percentage></percentage><bottomLabel></bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "Gún bē kā Bernie講。",
|
||||
"annual_report.summary.share_message": "我得著 {archetype} ê典型!",
|
||||
"annual_report.summary.thanks": "多謝成做Mastodon ê成員!",
|
||||
"attachments_list.unprocessed": "(Iáu bē處理)",
|
||||
"audio.hide": "Tshàng聲音",
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
"annual_report.summary.new_posts.new_posts": "nieuwe berichten",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Hiermee behoor je tot de top</topLabel><percentage></percentage><bottomLabel> van {domain}.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "We zullen Bernie niets vertellen.",
|
||||
"annual_report.summary.share_message": "Ik heb het archetype {archetype}!",
|
||||
"annual_report.summary.thanks": "Bedankt dat je deel uitmaakt van Mastodon!",
|
||||
"attachments_list.unprocessed": "(niet verwerkt)",
|
||||
"audio.hide": "Audio verbergen",
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"account.edit_profile_short": "แก้ไข",
|
||||
"account.enable_notifications": "แจ้งเตือนฉันเมื่อ @{name} โพสต์",
|
||||
"account.endorse": "แสดงในโปรไฟล์",
|
||||
"account.familiar_followers_one": "ติดตามโดย {name1}",
|
||||
"account.familiar_followers_two": "ติดตามโดย {name1} และ {name2}",
|
||||
"account.featured": "น่าสนใจ",
|
||||
"account.featured.accounts": "โปรไฟล์",
|
||||
"account.featured.hashtags": "แฮชแท็ก",
|
||||
@@ -65,6 +67,7 @@
|
||||
"account.mute_short": "ซ่อน",
|
||||
"account.muted": "ซ่อนอยู่",
|
||||
"account.muting": "กำลังซ่อน",
|
||||
"account.mutual": "คุณติดตามกันและกัน",
|
||||
"account.no_bio": "ไม่ได้ให้คำอธิบาย",
|
||||
"account.open_original_page": "เปิดหน้าดั้งเดิม",
|
||||
"account.posts": "โพสต์",
|
||||
@@ -232,6 +235,7 @@
|
||||
"confirmations.missing_alt_text.secondary": "โพสต์ต่อไป",
|
||||
"confirmations.missing_alt_text.title": "เพิ่มข้อความแสดงแทน?",
|
||||
"confirmations.mute.confirm": "ซ่อน",
|
||||
"confirmations.private_quote_notify.confirm": "เผยแพร่โพสต์",
|
||||
"confirmations.quiet_post_quote_info.dismiss": "ไม่ต้องเตือนฉันอีก",
|
||||
"confirmations.quiet_post_quote_info.got_it": "เข้าใจแล้ว",
|
||||
"confirmations.redraft.confirm": "ลบแล้วร่างใหม่",
|
||||
@@ -381,6 +385,7 @@
|
||||
"follow_suggestions.who_to_follow": "ติดตามใครดี",
|
||||
"followed_tags": "แฮชแท็กที่ติดตาม",
|
||||
"footer.about": "เกี่ยวกับ",
|
||||
"footer.about_this_server": "เกี่ยวกับ",
|
||||
"footer.directory": "ไดเรกทอรีโปรไฟล์",
|
||||
"footer.get_app": "รับแอป",
|
||||
"footer.keyboard_shortcuts": "แป้นพิมพ์ลัด",
|
||||
@@ -417,6 +422,7 @@
|
||||
"hints.profiles.see_more_followers": "ดูผู้ติดตามเพิ่มเติมใน {domain}",
|
||||
"hints.profiles.see_more_follows": "ดูการติดตามเพิ่มเติมใน {domain}",
|
||||
"hints.profiles.see_more_posts": "ดูโพสต์เพิ่มเติมใน {domain}",
|
||||
"home.column_settings.show_quotes": "แสดงการอ้างอิง",
|
||||
"home.column_settings.show_reblogs": "แสดงการดัน",
|
||||
"home.column_settings.show_replies": "แสดงการตอบกลับ",
|
||||
"home.hide_announcements": "ซ่อนประกาศ",
|
||||
@@ -461,6 +467,7 @@
|
||||
"keyboard_shortcuts.home": "เปิดเส้นเวลาหน้าแรก",
|
||||
"keyboard_shortcuts.hotkey": "ปุ่มลัด",
|
||||
"keyboard_shortcuts.legend": "แสดงคำอธิบายนี้",
|
||||
"keyboard_shortcuts.load_more": "โฟกัสปุ่ม \"โหลดเพิ่มเติม\"",
|
||||
"keyboard_shortcuts.local": "เปิดเส้นเวลาในเซิร์ฟเวอร์",
|
||||
"keyboard_shortcuts.mention": "กล่าวถึงผู้สร้าง",
|
||||
"keyboard_shortcuts.muted": "เปิดรายการผู้ใช้ที่ซ่อนอยู่",
|
||||
@@ -469,6 +476,7 @@
|
||||
"keyboard_shortcuts.open_media": "เปิดสื่อ",
|
||||
"keyboard_shortcuts.pinned": "เปิดรายการโพสต์ที่ปักหมุด",
|
||||
"keyboard_shortcuts.profile": "เปิดโปรไฟล์ของผู้สร้าง",
|
||||
"keyboard_shortcuts.quote": "อ้างอิงโพสต์",
|
||||
"keyboard_shortcuts.reply": "ตอบกลับโพสต์",
|
||||
"keyboard_shortcuts.requests": "เปิดรายการคำขอติดตาม",
|
||||
"keyboard_shortcuts.search": "โฟกัสแถบค้นหา",
|
||||
@@ -546,6 +554,8 @@
|
||||
"navigation_bar.follows_and_followers": "การติดตามและผู้ติดตาม",
|
||||
"navigation_bar.import_export": "การนำเข้าและการส่งออก",
|
||||
"navigation_bar.lists": "รายการ",
|
||||
"navigation_bar.live_feed_local": "ฟีดสด (ในเซิร์ฟเวอร์)",
|
||||
"navigation_bar.live_feed_public": "ฟีดสด (สาธารณะ)",
|
||||
"navigation_bar.logout": "ออกจากระบบ",
|
||||
"navigation_bar.moderation": "การกลั่นกรอง",
|
||||
"navigation_bar.more": "เพิ่มเติม",
|
||||
@@ -554,6 +564,7 @@
|
||||
"navigation_bar.preferences": "การกำหนดลักษณะ",
|
||||
"navigation_bar.privacy_and_reach": "ความเป็นส่วนตัวและการเข้าถึง",
|
||||
"navigation_bar.search": "ค้นหา",
|
||||
"navigation_bar.search_trends": "ค้นหา / กำลังนิยม",
|
||||
"navigation_panel.collapse_lists": "ยุบเมนูรายการ",
|
||||
"navigation_panel.expand_lists": "ขยายเมนูรายการ",
|
||||
"not_signed_in_indicator.not_signed_in": "คุณจำเป็นต้องเข้าสู่ระบบเพื่อเข้าถึงทรัพยากรนี้",
|
||||
@@ -576,6 +587,7 @@
|
||||
"notification.label.mention": "การกล่าวถึง",
|
||||
"notification.label.private_mention": "การกล่าวถึงแบบส่วนตัว",
|
||||
"notification.label.private_reply": "การตอบกลับแบบส่วนตัว",
|
||||
"notification.label.quote": "{name} ได้อ้างอิงโพสต์ของคุณ",
|
||||
"notification.label.reply": "การตอบกลับ",
|
||||
"notification.mention": "การกล่าวถึง",
|
||||
"notification.mentioned_you": "{name} ได้กล่าวถึงคุณ",
|
||||
@@ -590,6 +602,7 @@
|
||||
"notification.moderation_warning.action_suspend": "ระงับบัญชีของคุณแล้ว",
|
||||
"notification.own_poll": "การสำรวจความคิดเห็นของคุณได้สิ้นสุดแล้ว",
|
||||
"notification.poll": "การสำรวจความคิดเห็นที่คุณได้ลงคะแนนได้สิ้นสุดแล้ว",
|
||||
"notification.quoted_update": "{name} ได้แก้ไขโพสต์ที่คุณได้อ้างอิง",
|
||||
"notification.reblog": "{name} ได้ดันโพสต์ของคุณ",
|
||||
"notification.reblog.name_and_others_with_link": "{name} และ <a>{count, plural, other {# อื่น ๆ}}</a> ได้ดันโพสต์ของคุณ",
|
||||
"notification.relationships_severance_event": "สูญเสียการเชื่อมต่อกับ {name}",
|
||||
@@ -633,6 +646,7 @@
|
||||
"notifications.column_settings.mention": "การกล่าวถึง:",
|
||||
"notifications.column_settings.poll": "ผลลัพธ์การสำรวจความคิดเห็น:",
|
||||
"notifications.column_settings.push": "การแจ้งเตือนแบบผลัก",
|
||||
"notifications.column_settings.quote": "การอ้างอิง:",
|
||||
"notifications.column_settings.reblog": "การดัน:",
|
||||
"notifications.column_settings.show": "แสดงในคอลัมน์",
|
||||
"notifications.column_settings.sound": "เล่นเสียง",
|
||||
@@ -708,7 +722,11 @@
|
||||
"privacy.private.short": "ผู้ติดตาม",
|
||||
"privacy.public.long": "ใครก็ตามที่อยู่ในและนอก Mastodon",
|
||||
"privacy.public.short": "สาธารณะ",
|
||||
"privacy.quote.anyone": "{visibility}, ใครก็ตามสามารถอ้างอิง",
|
||||
"privacy.quote.disabled": "{visibility}, ปิดใช้งานการอ้างอิงแล้ว",
|
||||
"privacy.quote.limited": "{visibility}, จำกัดการอ้างอิงอยู่",
|
||||
"privacy.unlisted.additional": "สิ่งนี้ทำงานเหมือนกับสาธารณะทุกประการ ยกเว้นโพสต์จะไม่ปรากฏในฟีดสดหรือแฮชแท็ก, การสำรวจ หรือการค้นหา Mastodon แม้ว่าคุณได้เลือกรับทั่วทั้งบัญชีก็ตาม",
|
||||
"privacy.unlisted.long": "ซ่อนจากผลลัพธ์การค้นหา, กำลังนิยม และเส้นเวลาสาธารณะของ Mastodon",
|
||||
"privacy.unlisted.short": "สาธารณะแบบเงียบ",
|
||||
"privacy_policy.last_updated": "อัปเดตล่าสุดเมื่อ {date}",
|
||||
"privacy_policy.title": "นโยบายความเป็นส่วนตัว",
|
||||
@@ -849,6 +867,8 @@
|
||||
"status.mute_conversation": "ซ่อนการสนทนา",
|
||||
"status.open": "ขยายโพสต์นี้",
|
||||
"status.pin": "ปักหมุดในโปรไฟล์",
|
||||
"status.quote_error.limited_account_hint.action": "แสดงต่อไป",
|
||||
"status.quote_post_author": "อ้างอิงโพสต์โดย @{name}",
|
||||
"status.read_more": "อ่านเพิ่มเติม",
|
||||
"status.reblog": "ดัน",
|
||||
"status.reblogged_by": "{name} ได้ดัน",
|
||||
@@ -916,5 +936,11 @@
|
||||
"video.pause": "หยุดชั่วคราว",
|
||||
"video.play": "เล่น",
|
||||
"video.unmute": "เลิกปิดเสียง",
|
||||
"visibility_modal.button_title": "ตั้งการมองเห็น",
|
||||
"visibility_modal.header": "การมองเห็นและการโต้ตอบ",
|
||||
"visibility_modal.privacy_label": "การมองเห็น",
|
||||
"visibility_modal.quote_followers": "ผู้ติดตามเท่านั้น",
|
||||
"visibility_modal.quote_nobody": "แค่ฉัน",
|
||||
"visibility_modal.quote_public": "ใครก็ตาม",
|
||||
"visibility_modal.save": "บันทึก"
|
||||
}
|
||||
|
||||
@@ -319,8 +319,8 @@
|
||||
"domain_pill.your_handle": "您的帳號:",
|
||||
"domain_pill.your_server": "您數位世界的家,您所有的嘟文都在這裡。不喜歡這台伺服器嗎?您能隨時搬家至其他伺服器並且仍保有您的跟隨者。",
|
||||
"domain_pill.your_username": "您於您的伺服器中獨一無二的識別。於不同的伺服器上可能找到具有相同帳號的使用者。",
|
||||
"dropdown.empty": "選項",
|
||||
"embed.instructions": "若您欲於您的網站嵌入此嘟文,請複製以下程式碼。",
|
||||
"dropdown.empty": "請選擇一個選項",
|
||||
"embed.instructions": "請複製以下程式碼以於您的網站嵌入此嘟文。",
|
||||
"embed.preview": "它將顯示成這樣:",
|
||||
"emoji_button.activity": "活動",
|
||||
"emoji_button.clear": "清除",
|
||||
@@ -344,7 +344,7 @@
|
||||
"empty_column.account_suspended": "帳號已被停權",
|
||||
"empty_column.account_timeline": "這裡還沒有嘟文!",
|
||||
"empty_column.account_unavailable": "無法取得個人檔案",
|
||||
"empty_column.blocks": "您還沒有封鎖任何使用者。",
|
||||
"empty_column.blocks": "您尚未封鎖任何使用者。",
|
||||
"empty_column.bookmarked_statuses": "您還沒有新增任何書籤。當您新增書籤時,它將於此顯示。",
|
||||
"empty_column.community": "本站時間軸是空的。快公開嘟些文搶頭香啊!",
|
||||
"empty_column.direct": "您還沒有收到任何私訊。當您私訊別人或收到私訊時,它將於此顯示。",
|
||||
@@ -356,12 +356,12 @@
|
||||
"empty_column.follow_requests": "您還沒有收到任何跟隨請求。當您收到的跟隨請求時,它將於此顯示。",
|
||||
"empty_column.followed_tags": "您還沒有跟隨任何主題標籤。當您跟隨主題標籤時,它們將於此顯示。",
|
||||
"empty_column.hashtag": "這個主題標籤下什麼也沒有。",
|
||||
"empty_column.home": "您的首頁時間軸是空的!跟隨更多人來將它填滿吧!",
|
||||
"empty_column.home": "您的首頁時間軸是空的!跟隨更多人將它填滿吧!",
|
||||
"empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出新的嘟文時,它們將顯示於此。",
|
||||
"empty_column.mutes": "您尚未靜音任何使用者。",
|
||||
"empty_column.notification_requests": "清空啦!已經沒有任何推播通知。當您收到新推播通知時,它們將依照您的設定於此顯示。",
|
||||
"empty_column.notifications": "您還沒有收到任何推播通知,當您與別人開始互動時,它將於此顯示。",
|
||||
"empty_column.public": "這裡什麼都沒有!嘗試寫些公開的嘟文,或者跟隨其他伺服器的使用者後,就會有嘟文出現了",
|
||||
"empty_column.public": "這裡什麼都沒有!嘗試寫些公開的嘟文,或著自己跟隨其他伺服器的使用者後就會有嘟文出現了",
|
||||
"error.no_hashtag_feed_access": "加入或登入 Mastodon 以檢視與跟隨此主題標籤。",
|
||||
"error.unexpected_crash.explanation": "由於發生系統故障或瀏覽器相容性問題,無法正常顯示此頁面。",
|
||||
"error.unexpected_crash.explanation_addons": "此頁面無法被正常顯示,這可能是由瀏覽器附加元件或網頁自動翻譯工具造成的。",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user