Compare commits

...

117 Commits

Author SHA1 Message Date
Eugen Rochko
8963f8c3c2 Bump version to 1.4.1 2017-05-28 19:30:38 +02:00
Nolan Lawson
5e41c26203 Use immutable list in UploadButton to avoid wasteful re-render (#3394) 2017-05-28 19:15:35 +02:00
Clworld
45837c533e Re-add stream end log for WebSocket (#3397) 2017-05-28 19:14:44 +02:00
Eugen Rochko
3fa8512474 Fix video having black border on top due to regression from #2608 (#3392)
The combination of object-fit, relative position 50% from top and translating it
back upwards 50% is what allows us to crop the video properly, so it needs to
be +50%-50%
2017-05-28 19:11:47 +02:00
ster
0e20de9f89 Ukrainian translation: fix typo (#3393) 2017-05-28 19:11:29 +02:00
Nolan Lawson
24d645b7d0 Fix IntersectionObserver isIntersecting in Edge (#3365) 2017-05-28 16:45:42 +02:00
Eugen Rochko
7b23f79d41 Bump version to 1.4.0.6 2017-05-28 16:32:53 +02:00
Eugen Rochko
3b4095cf1b Update bootsnap to 0.3.0 (fix xattr.h error) (#3390) 2017-05-28 16:32:29 +02:00
Nolan Lawson
28cbfb9f10 Simplify isIntersecting in status_list.js (#3371) 2017-05-28 16:26:35 +02:00
Akihiko Odaki
189a06d2a2 Fix Webpack Bundle Analyzer output for Webpacker (#3374)
Webpacker failed to parse output of Webpack when a module requires
non-existent module or has similar errors. This commit fixes the bug.
2017-05-28 16:26:16 +02:00
vidarlee
450441fc11 i18n: Add some tags for Chinese translation (#3379)
* Enhance for the Chinese translation

* Add filtered_languages for Chinese Translation include CN/HK/TW for PR #3175

* i18n: Add some tags for Chinese translation

* i18n: Add some tags for Chinese translation
2017-05-28 16:25:54 +02:00
Masoud Abkenar
b619362a36 Persian translation fixes and updates (#3380)
* Persian translation fix

* Persian translation of new strings
2017-05-28 16:25:45 +02:00
Eugen Rochko
425d02287a Improve streaming API cluster logging (#3370)
* Improve streaming API cluster logging

* Less verbose error middleware logging (stack trace useless there)

* Fix error logging

* Prevent potential issue

* Add missing "done()" in catch of Promise.all, websocket heartbeat re-implemented like in example

* I actually forgot a done(), the absolute madman
2017-05-28 16:25:26 +02:00
Clworld
2e429c0c25 Reject revoked access_token on Streaming API. (#3367) 2017-05-27 23:27:54 +02:00
Eugen Rochko
e0e12b0fee Bump version to 1.4.0.5 2017-05-27 16:56:47 +02:00
Eugen Rochko
62ca37884a Fix #2922 - Load stylesheet from "custom.css" entrypoint when present (#3332)
* Fix #2922 - Load stylesheet from "custom.css" entrypoint when present

This is pretty much the same way it worked as before, albeit with
having to create app/javascript/packs/custom.js with
require('../styles/custom.scss') (or whatever you want really), which
will be a blank slate for you to import whatever you want

* Remove old assets directory

* Extract font-awesome into common.css and always load it
2017-05-27 16:55:09 +02:00
jeroenpraat
f9180823bc Update Dutch strings for 1.4 (#3363)
* Update nl strings for 1.4

* Update nl strings for 1.4

* Update nl strings for 1.4

* nl strings (+1)

More new OTP strings will be translated another time
2017-05-27 16:28:32 +02:00
Quent-in
4b0c667c09 i18n update for Occitan language (#3362)
* Update simple_form.oc.yml

* Added check spam folder

* Update oc.json

abonats => seguidors

* Update oc.yml

* Update oc.json

Added translations + corrections

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb

* i18n mailer

* Update reset_password_instructions.oc.html.erb
2017-05-27 15:50:40 +02:00
m4sk1n
1b732cad61 i18n: pl: pluralized (#3344)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-05-27 13:04:42 +02:00
unarist
ecef03bb15 Fix empty flash message on the settings page (#3345) 2017-05-27 13:04:28 +02:00
happycoloredbanana
9642601126 Avoid comparing domains when looking for an exact match of a local account (#3336) 2017-05-27 00:55:08 +02:00
Eugen Rochko
3836d293a1 Add missing background center on public profile headers (#3340) 2017-05-27 00:54:00 +02:00
Eugen Rochko
0734e1fe33 Language filtering in streaming API (#3339) 2017-05-27 00:53:48 +02:00
Eugen Rochko
44cb08297c Fix some nil errors (#3338)
* Fix nil input not handled well in AuthorExtractor concern

* Fix hard error in ProcessFeedService when replied-to status has been deleted

* Fix nil errors in ProcessInteractionService when favourited status
cannot be found
2017-05-27 00:53:38 +02:00
Eugen Rochko
bd21afb5ed Replace onboarding elephant with friendlier graphic, shorter animation (#3337)
on status fade-in, fix buttons not having pointer cursor
2017-05-27 00:53:25 +02:00
m4sk1n
ef80ad17b3 Updated Polish translation (#3335)
* i18n: updated Polish translation

Signed-off-by: Marcin Mikołajczak <m4sk1n@vivaldi.net>

* i18n: completed Polish translation

Signed-off-by: Marcin Mikołajczak <m4sk1n@vivaldi.net>

* i18n: corrected Polish translation

Signed-off-by: Marcin Mikołajczak <m4sk1n@vivaldi.net>

* i18n: Updated Polish translation

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>

* Update simple_form.pl.yml

* Update simple_form.pl.yml

* updated Polish translation

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>

* Update pl.yml
2017-05-26 19:59:46 +02:00
Eugen Rochko
9ea4f37e78 Add "meta" attribute to return of POST /api/v1/media method as well (#3333) 2017-05-26 18:22:30 +02:00
Eugen Rochko
c48772fd3f Introduce react-textarea-autosize instead of using style.height side effects (#3334) 2017-05-26 18:22:23 +02:00
Akihiko Odaki
860e257a68 Remove redundant call of recent scope in AccountsController (#3330)
recent is included in paginate_by_max_id.
2017-05-26 16:35:25 +02:00
unarist
902d9e34b4 Remove status context construction in the React side (#3331)
because it may causes flicker on the conversation when it contains blocked/muted user's status.

We use `/api/v1/statuses/{id}/context` to obtain status ids in the
conversation which filters blocked/muted user, but also uses internal
cache constructed from `in_reply_to_id` by `normalizeStatus()` in
`reducers/timelines.js` on each status loading which doesn't filter.

So statuses appears in conversation if those are cached, even those
statuses are from blocked/muted user. Then context cache will be updated
with the result of the context API and those statuses will be removed.

I have left the `normalizeStatus()` function itself which is called many
functions in the file as a placeholder for now, but maybe it should be
removed completely.
2017-05-26 16:34:08 +02:00
Eugen Rochko
2d97c898f2 Bump version to 1.4.0.4 2017-05-26 14:16:04 +02:00
Akinori MUSHA
f6a93fc150 Go to root after login in single user mode (#3289)
In single user mode, visitors are redirected to the single user's
profile page.  So, if you are the owner without a session, you start
from that page, click the login button and authenticate yourself
expecting you'll soon get started with the home page, but in reality
you'll get redirected back to where you started from -- your own
profile page.

This fixes the behavior by redirecting you home after login if you
have started from your own profile page.
2017-05-26 14:14:03 +02:00
Akihiko Odaki
019f3377bb Spec Admin::BaseController (#3328) 2017-05-26 14:13:26 +02:00
Yamagishi Kazutoshi
4b11675bdc Change anchor to button element (#3321)
Fix warning for ESLint (jsx-a11y/href-no-hash).
2017-05-26 14:10:37 +02:00
Nolan Lawson
2531c5953b Replace onWheel listener with onScroll (#3324)
* Replace onWheel listener with onScroll

* Update column.js
2017-05-26 14:09:13 +02:00
unarist
c6db416ff7 Fix "Cannot read property" on missing status (#3322)
I've found this issue when I clicked replies to muted user on the timeline.

Properties I've removed in here were added with lazy loading using
IntersectionObserver (8e4d1cba), but those statuses are not need to be
tracked anyway because it will be rendered as only empty div.
2017-05-26 14:07:48 +02:00
Nolan Lawson
b00cb2aed3 Improve shouldComponentUpdate for status and status_action_bar (#3323) 2017-05-26 14:05:52 +02:00
Nolan Lawson
7c67cb5997 implement shouldComponentUpdate for relative_timestamp (#3320) 2017-05-26 03:25:41 +02:00
Eugen Rochko
a098d08d12 Fix style regression of buttons not inheriting document font by default (#3310) 2017-05-25 21:15:41 +02:00
Matt Jankowski
6267759607 Disable the Rubocop Style/GuardClause cop (#3316)
There are many spots throughout the codebase which are showing as covered by
specs in the simplecov output -- but which are not actually run, because they
are on the same line as a guard clause.

I plan on fixing some of these issues, but don't want to keep triggering this
rubocop style violation.

My preference would be that we use the PR review process to identify places
where a guard clause might be appropriate, but that we leave this cop turned off
by default.
2017-05-25 21:15:14 +02:00
Nolan Lawson
bc39ad37c4 Fix "contains" CSS for Chromium <57 (#3317)
Fixes #3277. I've tested in QupZilla on Mac and can confirm it fixes the issue, and that Chrome 58 is not affected (because it has Grid).
2017-05-25 21:14:56 +02:00
Nolan Lawson
a6ba004bf5 Attach ReactPerf to window for easier debugging (#3318) 2017-05-25 21:14:40 +02:00
unarist
b89ab7e69d Add flex: 0 0 auto to some components to avoid bugs on iOS9 (#3313) 2017-05-25 18:59:55 +02:00
Nolan Lawson
33d7338779 Fix Firefox issue with performance marks (#3315) 2017-05-25 18:59:18 +02:00
unarist
cf4fe6cab8 More use of next link header on account (media) timelines (#3311)
This will reduce requests on who have only few statuses.

- Use next link header to detect more items from first request
- Omit next link header if result items are fewer than requested count
(It had omit it only if result was empty before)
2017-05-25 17:09:13 +02:00
Ira
2241a15ee9 i18n: New strings translated to Hebrew (#3282) 2017-05-25 16:38:02 +02:00
Yamagishi Kazutoshi
bca334cd28 Remove arguments in association reader (#3305) 2017-05-25 16:28:14 +02:00
unarist
3e3ec9b2c8 Fix an error on dragging into status_content component (#3308) 2017-05-25 16:27:44 +02:00
Akihiko Odaki
a8736aab7a Cover TagsController more (#3306) 2017-05-25 16:07:38 +02:00
Nolan Lawson
71b266377c Remove unnecessary action bar wrapper divs (#3299) 2017-05-25 14:15:12 +02:00
alpaca-tc
08dce5e607 Refactor Setting (#3302)
* @object is not needed

* Remove unneeded dependencies

* Do not call private method

* Prefer #respond_to_missing? over #respond_to?

`#respond_to?` doesn't support `User.settings.method(:method_name)`

* Use find_or_initialize_by instead of
2017-05-25 14:14:36 +02:00
Nolan Lawson
2469fd1cdc Add improved performance marks for development mode (#3297) 2017-05-25 14:09:55 +02:00
Yamagishi Kazutoshi
531c1bb245 Add localization of datetime in public page (#3296) 2017-05-25 14:09:25 +02:00
Audun Larsen
58f5040ee8 Norwegian translation of the onboarding page (#3303)
* Fixes translation placeholder

* Translates onboarding page
2017-05-25 14:06:10 +02:00
Daigo 3 Dango
838f51770b Replace \u2014 with &mdash; (#3301) 2017-05-25 14:05:54 +02:00
Yamagishi Kazutoshi
c52090dbfe fix storybook (#3292) 2017-05-25 05:23:14 +02:00
unarist
807c192fcf Fix load more feature on the Account media gallery (#3293)
* Add load more button for large screens
* Fix `next` state value on the first loading
* Don't load if `isLoading || !hasMore`
* Start load on near the bottom
2017-05-25 05:22:46 +02:00
masarakki
3b59f9c6c2 fix-cache-returns-nil (#3213) 2017-05-25 05:18:08 +02:00
Nolan Lawson
135bdd149e Name the Webpack chunks (#3291) 2017-05-25 03:30:29 +02:00
Yamagishi Kazutoshi
3572138b16 Fix hovering default value for avatar component (#3290) 2017-05-25 03:08:05 +02:00
unarist
9f69aa3cb1 Prevent contents of the status placeholder from overflowing (#3287)
Since long lines may overflow and cause the status-list horizontally scrollable,
I added `overflow: hidden` to placeholder contents to prevent it.
2017-05-25 02:23:54 +02:00
Daigo 3 Dango
f5c3d20e9c Add ffmpeg and dependent packages as well as LD_LIBRARY_PATHs (#3276)
This change adds mp4 support confirmed on Heroku-16 stack.
2017-05-24 17:57:33 +02:00
Yamagishi Kazutoshi
1ec7c87001 Remove unnecessary constructors (#3280) 2017-05-24 17:55:16 +02:00
Sorin Davidoi
8e4d1cba00 Lazy load toots using IntersectionObserver (#3191)
* refactor(components/status_list): Lazy load using IntersectionObserver

* refactor(components/status_list): Avoid setState bottleneck

* refactor(components/status_list): Update state correctly

* fix(components/status): Render if isIntersecting is undefined

* refactor(components/status): Recycle timeout

* refactor(components/status): Reduce animation duration

* refactor(components/status): Use requestIdleCallback

* chore: Split polyfill bundles

* refactor(components/status_list): Increase rootMargin to 300%

* fix(components/status): Check if onRef is not defined

* chore: Add note about polyfill bundle splitting

* fix(components/status): Reduce animation duration to 0.3 seconds
2017-05-24 17:55:00 +02:00
Yamagishi Kazutoshi
676ba50601 Show error message to suspended user (#3281) 2017-05-24 16:39:09 +02:00
abcang
bbc3db8b20 Add test when hashtag and URL are concatenated (#3279) 2017-05-24 15:36:10 +02:00
Yamagishi Kazutoshi
f937cad68f Skip formatting for cashtag in status text (#3275)
Resolve #3270
2017-05-24 14:32:53 +02:00
Eugen Rochko
be83d450eb Fix DM being highlighted when it's inside a favourite notification (#3267) 2017-05-24 04:22:46 +02:00
Nolan Lawson
1fd18a61bd Use node instead of babel-node for streaming (#3269) 2017-05-24 00:45:05 +02:00
Akihiko Odaki
5d9f479538 Cover HomeController more (#3258) 2017-05-23 23:37:24 +02:00
unarist
3ce9ca4c99 Fix following/followers API to return correct link headers (#3268)
Link headers in following/followers API should include follow_id as max_id/since_id.

However, these API use current_user's account_id instead of follow_id from #3167.
This causes irrelevant result on loading more users.
2017-05-23 23:26:23 +02:00
Eugen Rochko
2ca1f0737a Fix Devise destroy method being available to delete user record (#3266)
(You may think that we need account deletions, but this way would've just orphaned the db records)
2017-05-23 21:32:42 +02:00
beatrix
19ecde8fe7 don't notify me when my toot is faved by someone i muted (#3245) 2017-05-23 20:05:52 +02:00
alpaca-tc
7ee5fc5d68 Toggle sensitive from admin page (#3261) 2017-05-23 19:45:43 +02:00
Matt Jankowski
4289ed1d13 Refactor of API timeline actions (#3263)
- Increase coverage to exercise all parts of each action
- Move into namespace to share common code
- Misc refactor of each action for smaller methods, simpler code
2017-05-23 18:11:39 +02:00
Daniel Hunsaker
256e3adc1d Add Support for Nanobox (#1709)
* Nanobox Support

- Added support for running Mastodon using Nanobox, both for local development, and for deployment to production
- Dev mode tested and is working properly
- Deployment is undergoing test as of this writing. If it works, this line will be amended to state success; if not, one or more subsequent commits will provide fixes.

* [nanobox] Resolve Deploy Issues

Everything seems to work except routing to the streaming API. Will investigate with the Nanobox staff and make fix commits if needed.

Changes made:
- Also need `NODE_ENV` in production
- Node runs on `:4000`
- Use `envsubst` to commit `.env.production` values, since `dotEnv` packages don't always support referencing other variables
- Can't precompile assets after `transform` hook, but do this locally so it only has to be done once.
- Rails won't create `production.log` on its own, so we do this ourselves.
- Some `start` commands run from `/data/` for some reason, so use absolute paths in command arguments

* [nanobox] Update Ruby version

* [nanobox] Fix db.rake Ruby code style issues

* [nanobox] Minor Fixes

Some minor adjustments to improve functionality:

- Fixed routing to `web.stream` instances
- Adjust `.env.nanobox` to properly generate a default `SMTP_FROM_ADDRESS` via `envsubst`
- Update Nginx configs to properly support the needed HTTP version and headers for proper functionality (the streaming API doesn't work without some of these settings in place)

* [nanobox] Move usage info to docs repo

* [nanobox] Updates for 1.2.x

- Need to leave out `pkg-config` since Nanobox deploys without Ruby's headers - create a gem group to exclude the gem during Nanobox installs, but allow it to remain part of the default set otherwise
- Update cron jobs to cover new/updated Rake tasks
- Update `.env.nanobox` to include latest defaults and additions

* [nanobox] Fix for nokogumbo, added in 1.3.x

Apparently, nokogumbo (pulled in by sanitize, added with `OEmbed Support for PreviewCard` (#2337) - 88725d6) tries to install before nokogiri, despite needing nokogiri available to build properly. Instruct it to use the same settings as nokogiri does when building nokogiri directly, instead of via bundler.

* [nanobox] Set NODE_ENV during asset compile

The switch to WebPack will rely on the local value of the NODE_ENV evar, so set it to production during asset compilation.

* [nanobox] Rebase on master; update Nginx configs

- `pkg-config` Gem no longer causes issues in Nanobox, so revert the Gemfile change which allowed excluding it
- Update Nginx configuration files with latest recommendations from production documentation
- Rebase on master to Get This Merged™

Everything should be golden!
2017-05-23 16:54:44 +02:00
Akihiko Odaki
152b4d54e8 Cover StatusesController more (#3259) 2017-05-23 16:34:28 +02:00
Akihiko Odaki
ea2ef16ea4 Cover StreamEntriesController more and remove redundant instructions (#3257)
* Cover StreamEntriesController more

* Remove redundant instructions in StreamEntriesController
2017-05-23 15:04:23 +02:00
Akihiko Odaki
1d3e0a5060 Spec AuthorizeFollowsController assigns @account (#3246)
@account is used by the view.
2017-05-23 13:12:45 +02:00
Akihiko Odaki
bf575a1f5e Introduce recent to Follow (#3247)
Introduce recent to Follow, as Account and other models have.
This change also adds specs for the scope and the dependents.
2017-05-23 13:12:19 +02:00
Yamagishi Kazutoshi
860ffc0560 Focus the submit button (#3253)
Focus the submit button when confirmation modal is opened.

Also, changed cancellation link to button.
This makes the meaning clearer.
2017-05-23 13:10:41 +02:00
Audun Larsen
7eb4abe20a Updates Norwegian translation (#3252) 2017-05-23 13:09:53 +02:00
Ira
1baa75f79f i18n: Latest Hebew strings translated (#3250) 2017-05-23 13:08:16 +02:00
Yamagishi Kazutoshi
1d436a4322 Refactoring. Removed useless import. (#3254)
original: https://github.com/tootsuite/mastodon/pull/2618
2017-05-23 13:08:01 +02:00
Akihiko Odaki
8fd174298d Cover AccountsController more in spec (#3229)
* Introduce recent scope to Status and StreamEntry

Introduce recent scope to Status and StreamEntry as Account has.

* Cover AccountsController more in AccountsController
2017-05-23 02:53:01 +02:00
Eugen Rochko
9afd7dadbf Fix more locale regressions from #3055 (#3242) 2017-05-23 02:50:04 +02:00
Matt Jankowski
8e84177305 Use local default for postgres host in node streaming service (#3240)
This location varies across postgres installations, and it seems like the pg
package knows how to guess correctly on each system.
2017-05-22 20:27:24 -04:00
Matt Jankowski
a28ce13b3e Coverage for api/web/settings controller (#3238) 2017-05-22 23:12:28 +02:00
Matt Jankowski
e1b42e9aa0 Add coverage for ReportFilter and AccountFilter (#3236) 2017-05-22 15:50:58 -04:00
Eugen Rochko
b51398d0dd Bump version 2017-05-22 21:38:19 +02:00
Matt Jankowski
ec34ec63b1 Specs for cleanup workers (#3235)
* Add spec files for feed and media cleanup workers

* Add coverage for feed and media cleanup schedulers

* Clean up feed and media cleanup workers
2017-05-22 19:36:21 +02:00
Eugen Rochko
4a4733b397 Similarly to #2426, put creation of remote statuses in a transaction, (#3233)
so that public timeline/caching would not encounter incomplete data
2017-05-22 19:35:48 +02:00
Akihiko Odaki
bda7391221 Cover ApplicationController more in spec (#3230) 2017-05-22 17:58:57 +02:00
Akihiko Odaki
b9e8ffbd12 Cover AccountUnfollowController more in spec (#3228) 2017-05-22 17:58:49 +02:00
Tomonori Murakami
7966d3a872 Fix locale bug when change it on preferences (#3223) (#3232) 2017-05-22 17:58:41 +02:00
Akihiko Odaki
422e4d897b Cover AccountFollowController more in spec (#3227) 2017-05-22 16:29:48 +02:00
Akihiko Odaki
cb2707776f Cover AboutController more in spec (#3226) 2017-05-22 16:27:19 +02:00
Eugen Rochko
48e7a22e34 Fix locale regression from #3055 (#3231) 2017-05-22 16:26:54 +02:00
Ratmir Karabut
2bb5486357 Update Russian translation (#3225)
* Add Russian translation (ru)

* Fix a missing comma

* Fix the wording for better consistency

* Update Russian translation

* Arrange Russian setting alphabetically

* Fix syntax error

* Update Russian translation

* Fix formatting error

* Update Russian translation

* Update Russian translation

* Update ru.jsx

* Fix syntax error

* Remove two_factor_auth.warning (appears obsolete)

* Add missing strings in ru.yml

A lot of new strings translated, especially for the newly added admin section

* Fix translation consistency

* Update Russian translation

* Update Russian translation (pluralizations)

* Update Russian translation

* Update Russian translation
2017-05-22 16:03:50 +02:00
Nolan Lawson
60e2b951de Add webpack-bundle-analyzer for production builds (#3170) 2017-05-22 15:42:11 +02:00
Immae
a94c152fd3 Allow alternate domains for mastodon handlers (#3187) 2017-05-22 15:40:04 +02:00
Nolan Lawson
9d04de1c8d Only load Intl data for current language (#3130)
* Only load Intl data for current language

* Extract common chunk only from application.js and public.js

* Generate locale packs, avoid caching on window object
2017-05-22 15:06:06 +02:00
Yamagishi Kazutoshi
73e4468ff3 Change "Account.any?" to "Account.exists?" (#3217) 2017-05-22 15:02:30 +02:00
m4sk1n
fbbd80b40b Updated Polish translation (#3204)
* i18n: updated Polish translation

Signed-off-by: Marcin Mikołajczak <m4sk1n@vivaldi.net>

* i18n: completed Polish translation

Signed-off-by: Marcin Mikołajczak <m4sk1n@vivaldi.net>

* i18n: corrected Polish translation

Signed-off-by: Marcin Mikołajczak <m4sk1n@vivaldi.net>

* i18n: Updated Polish translation

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>

* Update simple_form.pl.yml

* Update simple_form.pl.yml
2017-05-22 15:01:53 +02:00
unarist
361a606edb Keep children of the column-collapsable until the transition is completed (#3218) 2017-05-22 15:01:27 +02:00
Clworld
df92f010ad Set config.cache_store in environments file. (#3219)
* Set config.cache_store in application.rb

* Set config.cache_store in environments.

* fix code format.
2017-05-22 15:01:02 +02:00
unarist
07af8c05fd Fix "Edit profile" on the account action bar (#3222) 2017-05-22 14:57:50 +02:00
Akihiko Odaki
aa662cecad single_user_mode? always returns boolean (#3215)
This change also adds a specification for the method.
2017-05-22 06:00:06 +02:00
Yamagishi Kazutoshi
84608c3ff8 Add translations for counter of profile (#3214)
ref #3101
2017-05-22 04:45:55 +02:00
Masoud Abkenar
b69365e397 Persian translation update (#3206)
* Persian translation update

* Persian translation update: new files

* Persian translation update

* activerecord.fa.yml language code

* Persian translation update

* fix indent
2017-05-22 00:20:13 +02:00
Eugen Rochko
a478af92c3 Fix forgotten to include language attribute in statuses JSON (#3209) 2017-05-21 21:38:09 +02:00
happycoloredbanana
7fba4cb3d1 Allow access token in URI (#3208) 2017-05-21 21:13:11 +02:00
ster
a4c757767f Update Ukrainian translation (#3203) 2017-05-21 18:52:50 +02:00
Akihiko Odaki
2af5cd96fe Fix mutes_controller error and incorrect statuses_controller report (#3202)
This commit fixes a regression in commit
f554807563.
2017-05-21 13:32:13 +02:00
Hayato IZUMI
860f408475 Update .gitignore (#3197)
* Remove duplicate 'node_modules'
* Replace 'public/packs'
2017-05-21 11:24:59 +02:00
vidarlee
440441ccb3 Enhance for the Chinese translation (#3198)
* Enhance for the Chinese translation

* Add filtered_languages for Chinese Translation include CN/HK/TW for PR #3175
2017-05-21 11:23:59 +02:00
Daigo 3 Dango
3eb13307ca Add a Japanese translation for PR #3175 (#3192) 2017-05-21 01:16:54 +02:00
198 changed files with 3453 additions and 1025 deletions

109
.env.nanobox Normal file
View File

@@ -0,0 +1,109 @@
# Service dependencies
# You may set REDIS_URL instead for more advanced options
REDIS_HOST=$DATA_REDIS_HOST
REDIS_PORT=6379
# REDIS_DB=0
# You may set DATABASE_URL instead for more advanced options
DB_HOST=$DATA_DB_HOST
DB_USER=$DATA_DB_USER
DB_NAME=gonano
DB_PASS=$DATA_DB_PASS
DB_PORT=5432
# Federation
# Note: Changing LOCAL_DOMAIN or LOCAL_HTTPS at a later time will cause unwanted side effects.
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
LOCAL_DOMAIN=${APP_NAME}.nanoapp.io
LOCAL_HTTPS=false
# Use this only if you need to run mastodon on a different domain than the one used for federation.
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING.
# WEB_DOMAIN=mastodon.example.com
# Use this if you want to have several aliases handler@example1.com
# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
# be added. Comma separated values
# ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets
# Generate each with the `rake secret` task (`nanobox run bundle exec rake secret`)
PAPERCLIP_SECRET=$PAPERCLIP_SECRET
SECRET_KEY_BASE=$SECRET_KEY_BASE
OTP_SECRET=$OTP_SECRET
# Registrations
# Single user mode will disable registrations and redirect frontpage to the first profile
# SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
# Optionally change default language
# DEFAULT_LOCALE=de
# E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
# If you want to use an SMTP server without authentication (e.g local Postfix relay)
# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and
# *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough).
SMTP_SERVER=$SMTP_SERVER
SMTP_PORT=587
SMTP_LOGIN=$SMTP_LOGIN
SMTP_PASSWORD=$SMTP_PASSWORD
SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
# PAPERCLIP_ROOT_URL=/system
# Optional asset host for multi-server setups
# CDN_HOST=assets.example.com
# S3 (optional)
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=http
# S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details)
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=
# S3_ENDPOINT=
# S3_SIGNATURE_VERSION=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST=
# Streaming API integration
# STREAMING_API_BASE_URL=
# Advanced settings
# If you need to use pgBouncer, you need to disable prepared statements:
# PREPARED_STATEMENTS=false
# Cluster number setting for streaming API server.
# If you comment out following line, cluster number will be `numOfCpuCores - 1`.
STREAMING_CLUSTER_NUM=1
# Docker mastodon user
# If you use Docker, you may want to assign UID/GID manually.
# UID=1000
# GID=1000

View File

@@ -20,6 +20,11 @@ LOCAL_HTTPS=true
# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING.
# WEB_DOMAIN=mastodon.example.com
# Use this if you want to have several aliases handler@example1.com
# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
# be added. Comma separated values
# ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
PAPERCLIP_SECRET=

4
.gitignore vendored
View File

@@ -19,6 +19,7 @@
coverage
public/system
public/assets
public/packs
.env
.env.production
node_modules/
@@ -43,9 +44,6 @@ redis
# Ignore vim files
*~
*.swp
/public/packs
/node_modules
# Ignore npm debug log
npm-debug.log

20
.nanoignore Normal file
View File

@@ -0,0 +1,20 @@
.DS_Store
.git/
.gitignore
.bundle/
.cache/
config/deploy/*
coverage
docs/
.env
log/*.log
neo4j/
node_modules/
public/assets/
public/system/
spec/
storybook/
tmp/
.vagrant/
vendor/bundle/

1
.profile Normal file
View File

@@ -0,0 +1 @@
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio

View File

@@ -74,6 +74,9 @@ Style/RegexpLiteral:
Style/Lambda:
Enabled: false
Style/GuardClause:
Enabled: false
Rails/HasAndBelongsToMany:
Enabled: false

View File

@@ -1,2 +1,5 @@
protobuf-compiler
libprotobuf-dev
ffmpeg
libxdamage1
libxfixes3

View File

@@ -19,7 +19,7 @@ gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6'
gem 'addressable', '~> 2.5'
gem 'bootsnap'
gem 'bootsnap', '~> 0.3'
gem 'cld3', '~> 3.1'
gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0'

View File

@@ -67,7 +67,7 @@ GEM
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (0.2.14)
bootsnap (0.3.0)
msgpack (~> 1.0)
brakeman (3.6.1)
builder (3.2.3)
@@ -474,7 +474,7 @@ DEPENDENCIES
aws-sdk (~> 2.9)
better_errors (~> 2.1)
binding_of_caller (~> 0.7)
bootsnap
bootsnap (~> 0.3)
brakeman (~> 3.6)
bullet (~> 5.5)
bundler-audit (~> 0.5)

View File

@@ -6,12 +6,12 @@ class AccountsController < ApplicationController
def show
respond_to do |format|
format.html do
@statuses = @account.statuses.permitted_for(@account, current_account).order(id: :desc).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
end
format.atom do
@entries = @account.stream_entries.order(id: :desc).where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
end

View File

@@ -2,17 +2,31 @@
module Admin
class ReportedStatusesController < BaseController
def destroy
status = Status.find params[:id]
before_action :set_report
before_action :set_status
RemovalWorker.perform_async(status.id)
redirect_to admin_report_path(report)
def update
@status.update(status_params)
redirect_to admin_report_path(@report)
end
def destroy
RemovalWorker.perform_async(@status.id)
redirect_to admin_report_path(@report)
end
private
def report
Report.find(params[:report_id])
def status_params
params.require(:status).permit(:sensitive)
end
def set_report
@report = Report.find(params[:report_id])
end
def set_status
@status = @report.statuses.find(params[:id])
end
end
end

View File

@@ -23,14 +23,14 @@ class Api::V1::AccountsController < ApiController
end
def following
@accounts = Account.includes(:followers)
.references(:followers)
@accounts = Account.includes(:passive_relationships)
.references(:passive_relationships)
.merge(Follow.where(account: @account)
.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]))
.to_a
next_path = following_api_v1_account_url(pagination_params(max_id: @accounts.last.followers.last.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = following_api_v1_account_url(pagination_params(since_id: @accounts.first.followers.first.id)) unless @accounts.empty?
next_path = following_api_v1_account_url(pagination_params(max_id: @accounts.last.passive_relationships.first.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = following_api_v1_account_url(pagination_params(since_id: @accounts.first.passive_relationships.first.id)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
@@ -38,16 +38,16 @@ class Api::V1::AccountsController < ApiController
end
def followers
@accounts = Account.includes(:following)
.references(:following)
@accounts = Account.includes(:active_relationships)
.references(:active_relationships)
.merge(Follow.where(target_account: @account)
.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]))
.to_a
next_path = followers_api_v1_account_url(pagination_params(max_id: @accounts.last.following.last.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = followers_api_v1_account_url(pagination_params(since_id: @accounts.first.following.first.id)) unless @accounts.empty?
next_path = followers_api_v1_account_url(pagination_params(max_id: @accounts.last.active_relationships.first.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = followers_api_v1_account_url(pagination_params(since_id: @accounts.first.active_relationships.first.id)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
@@ -62,7 +62,7 @@ class Api::V1::AccountsController < ApiController
set_maps(@statuses)
next_path = statuses_api_v1_account_url(statuses_pagination_params(max_id: @statuses.last.id)) unless @statuses.empty?
next_path = statuses_api_v1_account_url(statuses_pagination_params(max_id: @statuses.last.id)) if @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
prev_path = statuses_api_v1_account_url(statuses_pagination_params(since_id: @statuses.first.id)) unless @statuses.empty?
set_pagination_headers(next_path, prev_path)

View File

@@ -7,14 +7,14 @@ class Api::V1::MutesController < ApiController
respond_to :json
def index
@accounts = Account.includes(:muting)
.references(:muting)
@accounts = Account.includes(:muted_by)
.references(:muted_by)
.merge(Mute.where(account: current_account)
.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]))
.to_a
next_path = api_v1_mutes_url(pagination_params(max_id: @accounts.last.mutings_accounts.last.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = api_v1_mutes_url(pagination_params(since_id: @accounts.first.mutings_accounts.first.id)) unless @accounts.empty?
next_path = api_v1_mutes_url(pagination_params(max_id: @accounts.last.muted_by_ids.last)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = api_v1_mutes_url(pagination_params(since_id: @accounts.first.muted_by_ids.first)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
end

View File

@@ -47,14 +47,14 @@ class Api::V1::StatusesController < ApiController
end
def favourited_by
@accounts = Account.includes(statuses: :favourites)
.references(statuses: :favourites)
.where(statuses: { id: @status.id })
.merge(@status.favourites.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]))
@accounts = Account.includes(:favourites)
.references(:favourites)
.where(favourites: { status_id: @status.id })
.merge(Favourite.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]))
.to_a
next_path = favourited_by_api_v1_status_url(pagination_params(max_id: @accounts.last.statuses.last.favourites.last.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = favourited_by_api_v1_status_url(pagination_params(since_id: @accounts.first.statuses.first.favourites.first.id)) unless @accounts.empty?
next_path = favourited_by_api_v1_status_url(pagination_params(max_id: @accounts.last.favourites.last.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = favourited_by_api_v1_status_url(pagination_params(since_id: @accounts.first.favourites.first.id)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
module Api::V1::Timelines
class BaseController < ApiController
respond_to :json
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
private
def cache_collection(raw)
super(raw, Status)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
raise 'Override in child controllers'
end
def prev_path
raise 'Override in child controllers'
end
end
end

View File

@@ -0,0 +1,44 @@
# frozen_string_literal: true
module Api::V1::Timelines
class HomeController < BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:show]
before_action :require_user!, only: [:show]
def show
@statuses = load_statuses
end
private
def load_statuses
cached_home_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_home_statuses
cache_collection home_statuses
end
def home_statuses
account_home_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def account_home_feed
Feed.new(:home, current_account)
end
def next_path
api_v1_timelines_home_url pagination_params(max_id: @statuses.last.id)
end
def prev_path
api_v1_timelines_home_url pagination_params(since_id: @statuses.first.id)
end
end
end

View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true
module Api::V1::Timelines
class PublicController < BaseController
def show
@statuses = load_statuses
end
private
def load_statuses
cached_public_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_public_statuses
cache_collection public_statuses
end
def public_statuses
public_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def public_timeline_statuses
Status.as_public_timeline(current_account, params[:local])
end
def next_path
api_v1_timelines_public_url pagination_params(max_id: @statuses.last.id)
end
def prev_path
api_v1_timelines_public_url pagination_params(since_id: @statuses.first.id)
end
end
end

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
module Api::V1::Timelines
class TagController < BaseController
before_action :load_tag
def show
@statuses = load_statuses
end
private
def load_tag
@tag = Tag.find_by(name: params[:id].downcase)
end
def load_statuses
cached_tagged_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_tagged_statuses
cache_collection tagged_statuses
end
def tagged_statuses
if @tag.nil?
[]
else
tag_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
end
def tag_timeline_statuses
Status.as_tag_timeline(@tag, current_account, params[:local])
end
def next_path
api_v1_timelines_tag_url params[:id], pagination_params(max_id: @statuses.last.id)
end
def prev_path
api_v1_timelines_tag_url params[:id], pagination_params(since_id: @statuses.first.id)
end
end
end

View File

@@ -1,61 +0,0 @@
# frozen_string_literal: true
class Api::V1::TimelinesController < ApiController
before_action -> { doorkeeper_authorize! :read }, only: [:home]
before_action :require_user!, only: [:home]
respond_to :json
def home
@statuses = Feed.new(:home, current_account).get(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses)
set_maps(@statuses)
next_path = api_v1_home_timeline_url(pagination_params(max_id: @statuses.last.id)) unless @statuses.empty?
prev_path = api_v1_home_timeline_url(pagination_params(since_id: @statuses.first.id)) unless @statuses.empty?
set_pagination_headers(next_path, prev_path)
render :index
end
def public
@statuses = Status.as_public_timeline(current_account, params[:local]).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses)
set_maps(@statuses)
next_path = api_v1_public_timeline_url(pagination_params(max_id: @statuses.last.id)) unless @statuses.empty?
prev_path = api_v1_public_timeline_url(pagination_params(since_id: @statuses.first.id)) unless @statuses.empty?
set_pagination_headers(next_path, prev_path)
render :index
end
def tag
@tag = Tag.find_by(name: params[:id].downcase)
@statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses)
set_maps(@statuses)
next_path = api_v1_hashtag_timeline_url(params[:id], pagination_params(max_id: @statuses.last.id)) unless @statuses.empty?
prev_path = api_v1_hashtag_timeline_url(params[:id], pagination_params(since_id: @statuses.first.id)) unless @statuses.empty?
set_pagination_headers(next_path, prev_path)
render :index
end
private
def cache_collection(raw)
super(raw, Status)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
end

View File

@@ -6,10 +6,15 @@ class Api::Web::SettingsController < ApiController
before_action :require_user!
def update
setting = ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
setting.data = params[:data]
setting.save!
render_empty
end
private
def setting
@_setting ||= ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
end
end

View File

@@ -39,7 +39,7 @@ class ApplicationController < ActionController::Base
end
def check_suspension
head 403 if current_user.account.suspended?
forbidden if current_user.account.suspended?
end
protected
@@ -61,7 +61,7 @@ class ApplicationController < ActionController::Base
end
def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.first
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
end
def current_account

View File

@@ -6,6 +6,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
def destroy
not_found
end
protected
def build_resource(hash = nil)

View File

@@ -12,13 +12,13 @@ class Auth::SessionsController < Devise::SessionsController
def create
super do |resource|
remember_me(resource)
flash[:notice] = nil
flash.delete(:notice)
end
end
def destroy
super
flash[:notice] = nil
flash.delete(:notice)
end
protected
@@ -35,10 +35,10 @@ class Auth::SessionsController < Devise::SessionsController
params.require(:user).permit(:email, :password, :otp_attempt)
end
def after_sign_in_path_for(_resource)
def after_sign_in_path_for(resource)
last_url = stored_location_for(:user)
if [about_path].include?(last_url)
if home_paths(resource).include?(last_url)
root_path
else
last_url || root_path
@@ -81,4 +81,14 @@ class Auth::SessionsController < Devise::SessionsController
session[:otp_user_id] = user.id
render :two_factor
end
private
def home_paths(resource)
paths = [about_path]
if single_user_mode? && resource.is_a?(User)
paths << short_account_path(username: resource.account)
end
paths
end
end

View File

@@ -4,6 +4,6 @@ class FollowerAccountsController < ApplicationController
include AccountControllerConcern
def index
@follows = Follow.where(target_account: @account).order(id: :desc).page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
end
end

View File

@@ -4,6 +4,6 @@ class FollowingAccountsController < ApplicationController
include AccountControllerConcern
def index
@follows = Follow.where(account: @account).order(id: :desc).page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
end
end

View File

@@ -11,6 +11,7 @@ class Settings::PreferencesController < ApplicationController
user_settings.update(user_settings_params.to_h)
if current_user.update(user_params)
I18n.locale = current_user.locale
redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show

View File

@@ -11,12 +11,8 @@ class StreamEntriesController < ApplicationController
def show
respond_to do |format|
format.html do
return gone if @stream_entry.activity.nil?
if @stream_entry.activity_type == 'Status'
@ancestors = @stream_entry.activity.reply? ? cache_collection(@stream_entry.activity.ancestors(current_account), Status) : []
@descendants = cache_collection(@stream_entry.activity.descendants(current_account), Status)
end
@ancestors = @stream_entry.activity.reply? ? cache_collection(@stream_entry.activity.ancestors(current_account), Status) : []
@descendants = cache_collection(@stream_entry.activity.descendants(current_account), Status)
end
format.atom do
@@ -46,7 +42,7 @@ class StreamEntriesController < ApplicationController
@stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id])
@type = @stream_entry.activity_type.downcase
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil? || (@stream_entry.hidden? && (@stream_entry.activity_type != 'Status' || (@stream_entry.activity_type == 'Status' && !@stream_entry.activity.permitted?(current_account))))
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil? || (@stream_entry.hidden? && !@stream_entry.activity.permitted?(current_account))
end
def check_account_suspension

View File

@@ -23,7 +23,14 @@ module WellKnown
private
def username_from_resource
WebfingerResource.new(resource_param).username
resource_user = resource_param
username, domain = resource_user.split('@')
if Rails.configuration.x.alternate_domains.include?(domain)
resource_user = "#{username}@#{Rails.configuration.x.local_domain}"
end
WebfingerResource.new(resource_user).username
end
def pem_to_magic_key(public_key)

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
module StyleHelper
def stylesheet_for_layout
if asset_exist? 'custom.css'
'custom'
else
'application'
end
end
def asset_exist?(path)
true if Webpacker::Manifest.lookup(path)
rescue Webpacker::FileLoader::NotFoundError
false
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -115,7 +115,8 @@ export function fetchAccountTimeline(id, replace = false) {
dispatch(fetchAccountTimelineRequest(id, skipLoading));
api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => {
dispatch(fetchAccountTimelineSuccess(id, response.data, replace, skipLoading));
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchAccountTimelineSuccess(id, response.data, replace, skipLoading, next));
}).catch(error => {
dispatch(fetchAccountTimelineFail(id, error, skipLoading));
});
@@ -138,7 +139,8 @@ export function fetchAccountMediaTimeline(id, replace = false) {
dispatch(fetchAccountMediaTimelineRequest(id, skipLoading));
api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => {
dispatch(fetchAccountMediaTimelineSuccess(id, response.data, replace, skipLoading));
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchAccountMediaTimelineSuccess(id, response.data, replace, skipLoading, next));
}).catch(error => {
dispatch(fetchAccountMediaTimelineFail(id, error, skipLoading));
});
@@ -283,13 +285,14 @@ export function fetchAccountTimelineRequest(id, skipLoading) {
};
};
export function fetchAccountTimelineSuccess(id, statuses, replace, skipLoading) {
export function fetchAccountTimelineSuccess(id, statuses, replace, skipLoading, next) {
return {
type: ACCOUNT_TIMELINE_FETCH_SUCCESS,
id,
statuses,
replace,
skipLoading,
next,
};
};
@@ -311,13 +314,14 @@ export function fetchAccountMediaTimelineRequest(id, skipLoading) {
};
};
export function fetchAccountMediaTimelineSuccess(id, statuses, replace, skipLoading) {
export function fetchAccountMediaTimelineSuccess(id, statuses, replace, skipLoading, next) {
return {
type: ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS,
id,
statuses,
replace,
skipLoading,
next,
};
};

View File

@@ -27,22 +27,15 @@ class Account extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
};
constructor (props, context) {
super(props, context);
this.handleFollow = this.handleFollow.bind(this);
this.handleBlock = this.handleBlock.bind(this);
this.handleMute = this.handleMute.bind(this);
}
handleFollow () {
handleFollow = () => {
this.props.onFollow(this.props.account);
}
handleBlock () {
handleBlock = () => {
this.props.onBlock(this.props.account);
}
handleMute () {
handleMute = () => {
this.props.onMute(this.props.account);
}

View File

@@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;
@@ -51,23 +52,14 @@ class AutosuggestTextarea extends ImmutablePureComponent {
autoFocus: true,
};
constructor (props, context) {
super(props, context);
this.state = {
suggestionsHidden: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
this.onChange = this.onChange.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onSuggestionClick = this.onSuggestionClick.bind(this);
this.setTextarea = this.setTextarea.bind(this);
this.onPaste = this.onPaste.bind(this);
}
state = {
suggestionsHidden: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
onChange (e) {
onChange = (e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
if (token !== null && this.state.lastToken !== token) {
@@ -78,14 +70,10 @@ class AutosuggestTextarea extends ImmutablePureComponent {
this.props.onSuggestionsClearRequested();
}
// auto-resize textarea
e.target.style.height = 'auto';
e.target.style.height = `${e.target.scrollHeight}px`;
this.props.onChange(e);
}
onKeyDown (e) {
onKeyDown = (e) => {
const { suggestions, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
@@ -135,7 +123,7 @@ class AutosuggestTextarea extends ImmutablePureComponent {
this.props.onKeyDown(e);
}
onBlur () {
onBlur = () => {
// If we hide the suggestions immediately, then this will prevent the
// onClick for the suggestions themselves from firing.
// Setting a short window for that to take place before hiding the
@@ -145,7 +133,7 @@ class AutosuggestTextarea extends ImmutablePureComponent {
}, 100);
}
onSuggestionClick (e) {
onSuggestionClick = (e) => {
const suggestion = Number(e.currentTarget.getAttribute('data-index'));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
@@ -158,21 +146,17 @@ class AutosuggestTextarea extends ImmutablePureComponent {
}
}
setTextarea (c) {
setTextarea = (c) => {
this.textarea = c;
}
onPaste (e) {
onPaste = (e) => {
if (e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files);
e.preventDefault();
}
}
reset () {
this.textarea.style.height = 'auto';
}
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props;
const { suggestionsHidden, selectedSuggestion } = this.state;
@@ -184,8 +168,8 @@ class AutosuggestTextarea extends ImmutablePureComponent {
return (
<div className='autosuggest-textarea'>
<textarea
ref={this.setTextarea}
<Textarea
inputRef={this.setTextarea}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}

View File

@@ -19,7 +19,7 @@ class Avatar extends React.PureComponent {
};
state = {
hovering: true,
hovering: false,
};
handleMouseEnter = () => {

View File

@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
class Button extends React.PureComponent {
@@ -10,6 +11,7 @@ class Button extends React.PureComponent {
block: PropTypes.bool,
secondary: PropTypes.bool,
size: PropTypes.number,
className: PropTypes.string,
style: PropTypes.object,
children: PropTypes.node,
};
@@ -20,10 +22,18 @@ class Button extends React.PureComponent {
handleClick = (e) => {
if (!this.props.disabled) {
this.props.onClick();
this.props.onClick(e);
}
}
setRef = (c) => {
this.node = c;
}
focus() {
this.node.focus();
}
render () {
const style = {
padding: `0 ${this.props.size / 2.25}px`,
@@ -32,11 +42,17 @@ class Button extends React.PureComponent {
...this.props.style,
};
const className = classNames('button', this.props.className, {
'button-secondary': this.props.secondary,
'button--block': this.props.block,
});
return (
<button
className={`button ${this.props.secondary ? 'button-secondary' : ''} ${this.props.block ? 'button--block' : ''}`}
className={className}
disabled={this.props.disabled}
onClick={this.handleClick}
ref={this.setRef}
style={style}
>
{this.props.text || this.props.children}

View File

@@ -13,30 +13,35 @@ class ColumnCollapsable extends React.PureComponent {
state = {
collapsed: true,
animating: false,
};
handleToggleCollapsed = () => {
const currentState = this.state.collapsed;
this.setState({ collapsed: !currentState });
this.setState({ collapsed: !currentState, animating: true });
if (!currentState && this.props.onCollapse) {
this.props.onCollapse();
}
}
handleTransitionEnd = () => {
this.setState({ animating: false });
}
render () {
const { icon, title, fullHeight, children } = this.props;
const { collapsed } = this.state;
const { collapsed, animating } = this.state;
return (
<div className={`column-collapsable ${collapsed ? 'collapsed' : ''}`}>
<div className={`column-collapsable ${collapsed ? 'collapsed' : ''}`} onTransitionEnd={this.handleTransitionEnd}>
<div role='button' tabIndex='0' title={`${title}`} className='column-collapsable__button column-icon' onClick={this.handleToggleCollapsed}>
<i className={`fa fa-${icon}`} />
</div>
<div className='column-collapsable__content' style={{ height: `${fullHeight}px` }}>
{!collapsed && children}
{(!collapsed || animating) && children}
</div>
</div>
);

View File

@@ -33,11 +33,14 @@ class DropdownMenu extends React.PureComponent {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
e.preventDefault();
// Don't call e.preventDefault() when the item uses 'href' property.
// ex. "Edit profile" on the account action bar
if (typeof action === 'function') {
e.preventDefault();
action();
} else if (to) {
e.preventDefault();
this.context.router.push(to);
}

View File

@@ -3,9 +3,9 @@ import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
const LoadMore = ({ onClick }) => (
<a href="#" className='load-more' role='button' onClick={onClick}>
<button className='load-more' onClick={onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</a>
</button>
);
LoadMore.propTypes = {

View File

@@ -2,19 +2,40 @@ import React from 'react';
import { injectIntl, FormattedRelative } from 'react-intl';
import PropTypes from 'prop-types';
const RelativeTimestamp = ({ intl, timestamp }) => {
const date = new Date(timestamp);
return (
<time dateTime={timestamp} title={intl.formatDate(date, { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}>
<FormattedRelative value={date} />
</time>
);
const dateFormatOptions = {
hour12: false,
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
};
RelativeTimestamp.propTypes = {
intl: PropTypes.object.isRequired,
timestamp: PropTypes.string.isRequired,
};
class RelativeTimestamp extends React.Component {
static propTypes = {
intl: PropTypes.object.isRequired,
timestamp: PropTypes.string.isRequired,
};
shouldComponentUpdate (nextProps) {
// As of right now the locale doesn't change without a new page load,
// but we might as well check in case that ever changes.
return this.props.timestamp !== nextProps.timestamp ||
this.props.intl.locale !== nextProps.intl.locale;
}
render () {
const { timestamp, intl } = this.props;
const date = new Date(timestamp);
return (
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
<FormattedRelative value={date} />
</time>
);
}
}
export default injectIntl(RelativeTimestamp);

View File

@@ -32,12 +32,65 @@ class Status extends ImmutablePureComponent {
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onBlock: PropTypes.func,
onRef: PropTypes.func,
isIntersecting: PropTypes.bool,
me: PropTypes.number,
boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool,
};
state = {
isHidden: false,
}
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'account',
'wrapped',
'me',
'boostModal',
'autoPlayGif',
'muted',
]
updateOnStates = []
shouldComponentUpdate (nextProps, nextState) {
if (nextProps.isIntersecting === false && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter.
return this.props.isIntersecting !== false || !this.state.isHidden;
} else if (nextProps.isIntersecting !== false && this.props.isIntersecting === false) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
return true;
}
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
return super.shouldComponentUpdate(nextProps, nextState);
}
componentWillReceiveProps (nextProps) {
if (nextProps.isIntersecting === false && this.props.isIntersecting !== false) {
requestIdleCallback(() => this.setState({ isHidden: true }));
} else {
this.setState({ isHidden: !nextProps.isIntersecting });
}
}
handleRef = (node) => {
if (this.props.onRef) {
this.props.onRef(node);
if (node && node.children.length !== 0) {
this.height = node.clientHeight;
}
}
}
handleClick = () => {
const { status } = this.props;
this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
@@ -52,12 +105,22 @@ class Status extends ImmutablePureComponent {
}
render () {
let media = '';
let media = null;
let statusAvatar;
const { status, account, ...other } = this.props;
const { status, account, isIntersecting, onRef, ...other } = this.props;
const { isHidden } = this.state;
if (status === null) {
return <div />;
return null;
}
if (isIntersecting === false && isHidden) {
return (
<div ref={this.handleRef} data-id={status.get('id')} style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
</div>
);
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
@@ -70,7 +133,7 @@ class Status extends ImmutablePureComponent {
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
return (
<div className='status__wrapper'>
<div className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} >
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
@@ -98,7 +161,7 @@ class Status extends ImmutablePureComponent {
}
return (
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`}>
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import IconButton from './icon_button';
import DropdownMenu from './dropdown_menu';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -21,7 +22,7 @@ const messages = defineMessages({
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
});
class StatusActionBar extends React.PureComponent {
class StatusActionBar extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -43,6 +44,14 @@ class StatusActionBar extends React.PureComponent {
intl: PropTypes.object.isRequired,
};
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'me',
'withDismiss',
]
handleReplyClick = () => {
this.props.onReply(this.props.status, this.context.router);
}
@@ -128,9 +137,9 @@ class StatusActionBar extends React.PureComponent {
return (
<div className='status__action-bar'>
<div className='status__action-bar-button-wrapper'><IconButton title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /></div>
<div className='status__action-bar-button-wrapper'><IconButton disabled={reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='status__action-bar-button-wrapper'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} className='star-icon' /></div>
<IconButton className='status__action-bar-button' title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<div className='status__action-bar-dropdown'>
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction="right" ariaLabel="More"/>

View File

@@ -65,6 +65,10 @@ class StatusContent extends React.PureComponent {
}
handleMouseUp = (e) => {
if (!this.startXY) {
return;
}
const [ startX, startY ] = this.startXY;
const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];

View File

@@ -1,5 +1,4 @@
import React from 'react';
import Status from './status';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types';
@@ -27,6 +26,13 @@ class StatusList extends ImmutablePureComponent {
trackScroll: true,
};
state = {
isIntersecting: {},
intersectionCount: 0,
}
statusRefQueue = []
handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight;
@@ -43,6 +49,7 @@ class StatusList extends ImmutablePureComponent {
componentDidMount () {
this.attachScrollListener();
this.attachIntersectionObserver();
}
componentDidUpdate (prevProps) {
@@ -53,6 +60,57 @@ class StatusList extends ImmutablePureComponent {
componentWillUnmount () {
this.detachScrollListener();
this.detachIntersectionObserver();
}
attachIntersectionObserver () {
const onIntersection = (entries) => {
this.setState(state => {
entries.forEach(entry => {
const statusId = entry.target.getAttribute('data-id');
// Edge 15 doesn't support isIntersecting, but we can infer it from intersectionRatio
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/
state.isIntersecting[statusId] = entry.intersectionRatio > 0;
});
// isIntersecting is a map of DOM data-id's to booleans (true for
// intersecting, false for non-intersecting).
//
// We always want to return true in shouldComponentUpdate() if
// this object changes, because onIntersection() is only called if
// something has changed.
//
// Now, we *could* use an immutable map or some other structure to
// diff the full map, but that would be pointless because the browser
// has already informed us that something has changed. So we can just
// use a regular object, which will be diffed by ImmutablePureComponent
// based on reference equality (i.e. it's always "unchanged") and
// then we just increment intersectionCount to force a change.
return {
isIntersecting: state.isIntersecting,
intersectionCount: state.intersectionCount + 1,
};
});
};
const options = {
root: this.node,
rootMargin: '300% 0px',
};
this.intersectionObserver = new IntersectionObserver(onIntersection, options);
if (this.statusRefQueue.length) {
this.statusRefQueue.forEach(node => this.intersectionObserver.observe(node));
this.statusRefQueue = [];
}
}
detachIntersectionObserver () {
this.intersectionObserver.disconnect();
}
attachScrollListener () {
@@ -67,6 +125,15 @@ class StatusList extends ImmutablePureComponent {
this.node = c;
}
handleStatusRef = (node) => {
if (node && this.intersectionObserver) {
const statusId = node.getAttribute('data-id');
this.intersectionObserver.observe(node);
} else {
this.statusRefQueue.push(node);
}
}
handleLoadMore = (e) => {
e.preventDefault();
this.props.onScrollToBottom();
@@ -74,10 +141,11 @@ class StatusList extends ImmutablePureComponent {
render () {
const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;
const { isIntersecting } = this.state;
let loadMore = '';
let scrollableArea = '';
let unread = '';
let loadMore = null;
let scrollableArea = null;
let unread = null;
if (!isLoading && statusIds.size > 0 && hasMore) {
loadMore = <LoadMore onClick={this.handleLoadMore} />;
@@ -96,7 +164,7 @@ class StatusList extends ImmutablePureComponent {
{prepend}
{statusIds.map((statusId) => {
return <StatusContainer key={statusId} id={statusId} />;
return <StatusContainer key={statusId} id={statusId} isIntersecting={isIntersecting[statusId]} onRef={this.handleStatusRef} />;
})}
{loadMore}

View File

@@ -41,34 +41,12 @@ import FavouritedStatuses from '../features/favourited_statuses';
import Blocks from '../features/blocks';
import Mutes from '../features/mutes';
import Report from '../features/report';
import { IntlProvider, addLocaleData } from 'react-intl';
import ar from 'react-intl/locale-data/ar';
import bg from 'react-intl/locale-data/bg';
import ca from 'react-intl/locale-data/ca';
import de from 'react-intl/locale-data/de';
import en from 'react-intl/locale-data/en';
import eo from 'react-intl/locale-data/eo';
import es from 'react-intl/locale-data/es';
import fa from 'react-intl/locale-data/fa';
import fi from 'react-intl/locale-data/fi';
import fr from 'react-intl/locale-data/fr';
import he from 'react-intl/locale-data/he';
import hr from 'react-intl/locale-data/hr';
import hu from 'react-intl/locale-data/hu';
import id from 'react-intl/locale-data/id';
import it from 'react-intl/locale-data/it';
import ja from 'react-intl/locale-data/ja';
import nl from 'react-intl/locale-data/nl';
import no from 'react-intl/locale-data/no';
import oc from '../locales/locale-data/oc';
import pt from 'react-intl/locale-data/pt';
import ru from 'react-intl/locale-data/ru';
import uk from 'react-intl/locale-data/uk';
import zh from 'react-intl/locale-data/zh';
import tr from 'react-intl/locale-data/tr';
import getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store';
import createStream from '../stream';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const store = configureStore();
const initialState = JSON.parse(document.getElementById("initial-state").textContent);
@@ -78,33 +56,6 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
basename: '/web',
});
addLocaleData([
...ar,
...bg,
...ca,
...de,
...en,
...eo,
...es,
...fa,
...fi,
...fr,
...he,
...hr,
...hu,
...id,
...it,
...ja,
...nl,
...no,
...oc,
...pt,
...ru,
...uk,
...zh,
...tr,
]);
class Mastodon extends React.PureComponent {
componentDidMount() {
@@ -145,7 +96,7 @@ class Mastodon extends React.PureComponent {
store.dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
store.dispatch(updateNotifications(JSON.parse(data.payload), getMessagesForLocale(locale), locale));
store.dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
}
},
@@ -183,7 +134,7 @@ class Mastodon extends React.PureComponent {
const { locale } = this.props;
return (
<IntlProvider locale={locale} messages={getMessagesForLocale(locale)}>
<IntlProvider locale={locale} messages={messages}>
<Provider store={store}>
<Router history={browserHistory} render={applyRouterMiddleware(useScroll())}>
<Route path='/' component={UI}>

View File

@@ -0,0 +1,2 @@
import 'intersection-observer';
import 'requestidlecallback';

View File

@@ -17,6 +17,7 @@ import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container';
import { FormattedMessage } from 'react-intl';
import { ScrollContainer } from 'react-router-scroll';
import LoadMore from '../../components/load_more';
const mapStateToProps = (state, props) => ({
medias: getAccountGallery(state, Number(props.params.accountId)),
@@ -48,16 +49,30 @@ class AccountGallery extends ImmutablePureComponent {
}
}
handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
if (scrollTop === scrollHeight - clientHeight) {
handleScrollToBottom = () => {
if (this.props.hasMore) {
this.props.dispatch(expandAccountMediaTimeline(Number(this.props.params.accountId)));
}
}
handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight;
if (150 > offset && !this.props.isLoading) {
this.handleScrollToBottom();
}
}
handleLoadMore = (e) => {
e.preventDefault();
this.handleScrollToBottom();
}
render () {
const { medias, autoPlayGif, isLoading } = this.props;
const { medias, autoPlayGif, isLoading, hasMore } = this.props;
let loadMore = null;
if (!medias && isLoading) {
return (
@@ -67,6 +82,10 @@ class AccountGallery extends ImmutablePureComponent {
);
}
if (!isLoading && medias.size > 0 && hasMore) {
loadMore = <LoadMore onClick={this.handleLoadMore} />;
}
return (
<Column>
<ColumnBackButton />
@@ -87,6 +106,7 @@ class AccountGallery extends ImmutablePureComponent {
autoPlayGif={autoPlayGif}
/>
)}
{loadMore}
</div>
</div>
</ScrollContainer>

View File

@@ -67,7 +67,6 @@ class ComposeForm extends ImmutablePureComponent {
}
handleSubmit = () => {
this.autosuggestTextarea.reset();
this.props.onSubmit();
}

View File

@@ -49,7 +49,7 @@ class EmojiPickerDropdown extends React.PureComponent {
this.setState({active: true});
if (!EmojiPicker) {
this.setState({loading: true});
import('emojione-picker').then(TheEmojiPicker => {
import(/* webpackChunkName: "emojione_picker" */ 'emojione-picker').then(TheEmojiPicker => {
EmojiPicker = TheEmojiPicker.default;
this.setState({loading: false});
}).catch(err => {

View File

@@ -3,6 +3,8 @@ import IconButton from '../../../components/icon_button';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add media' },
@@ -10,7 +12,7 @@ const messages = defineMessages({
const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray(),
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
});
return mapStateToProps;
@@ -21,14 +23,14 @@ const iconStyle = {
lineHeight: '27px',
};
class UploadButton extends React.PureComponent {
class UploadButton extends ImmutablePureComponent {
static propTypes = {
disabled: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired,
style: PropTypes.object,
resetFileKey: PropTypes.number,
acceptContentTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
intl: PropTypes.object.isRequired,
};
@@ -58,7 +60,7 @@ class UploadButton extends React.PureComponent {
ref={this.setRef}
type='file'
multiple={false}
accept={ acceptContentTypes.join(',')}
accept={ acceptContentTypes.toArray().join(',')}
onChange={this.handleChange}
disabled={disabled}
style={{ display: 'none' }}

View File

@@ -27,12 +27,6 @@ class BoostModal extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
};
constructor (props, context) {
super(props, context);
this.handleReblog = this.handleReblog.bind(this);
this.handleAccountClick = this.handleAccountClick.bind(this);
}
handleReblog = () => {
this.props.onReblog(this.props.status);
this.props.onClose();

View File

@@ -1,6 +1,7 @@
import React from 'react';
import ColumnHeader from './column_header';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
const easingOutQuint = (x, t, b, c, d) => c*((t=t/d-1)*t*t*t*t + 1) + b;
@@ -48,11 +49,11 @@ class Column extends React.PureComponent {
this._interruptScrollAnimation = scrollTop(scrollable);
}
handleWheel = () => {
handleScroll = debounce(() => {
if (typeof this._interruptScrollAnimation !== 'undefined') {
this._interruptScrollAnimation();
}
}
}, 200)
setRef = (c) => {
this.node = c;
@@ -74,7 +75,7 @@ class Column extends React.PureComponent {
role='region'
aria-labelledby={columnHeaderId}
className='column'
onWheel={this.handleWheel}>
onScroll={this.handleScroll}>
{header}
{children}
</div>

View File

@@ -13,18 +13,25 @@ class ConfirmationModal extends React.PureComponent {
intl: PropTypes.object.isRequired,
};
componentDidMount() {
this.button.focus();
}
handleClick = () => {
this.props.onClose();
this.props.onConfirm();
}
handleCancel = (e) => {
e.preventDefault();
handleCancel = () => {
this.props.onClose();
}
setRef = (c) => {
this.button = c;
}
render () {
const { intl, message, confirm, onConfirm, onClose } = this.props;
const { message, confirm } = this.props;
return (
<div className='modal-root__modal confirmation-modal'>
@@ -33,8 +40,10 @@ class ConfirmationModal extends React.PureComponent {
</div>
<div className='confirmation-modal__action-bar'>
<div><a href='#' onClick={this.handleCancel}><FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /></a></div>
<Button text={confirm} onClick={this.handleClick} />
<Button onClick={this.handleCancel} className='confirmation-modal__cancel-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button>
<Button text={confirm} onClick={this.handleClick} ref={this.setRef} />
</div>
</div>
);

View File

@@ -191,15 +191,14 @@ class OnboardingModal extends React.PureComponent {
this.setState({ currentIndex: i });
}
handleNext = (e) => {
const maxNum = Number(e.currentTarget.getAttribute('data-length'));
e.preventDefault();
handleNext = () => {
this.setState(({ currentIndex }) => ({
currentIndex: currentIndex + 1,
}));
}
if (this.state.currentIndex < maxNum - 1) {
this.setState({ currentIndex: this.state.currentIndex + 1 });
} else {
this.props.onClose();
}
handleClose = () => {
this.props.onClose();
}
render () {
@@ -216,13 +215,21 @@ class OnboardingModal extends React.PureComponent {
const { currentIndex } = this.state;
const hasMore = currentIndex < pages.length - 1;
let nextOrDoneBtn;
if(hasMore) {
nextOrDoneBtn = <a href='#' data-length={pages.length} onClick={this.handleNext} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>;
} else {
nextOrDoneBtn = <a href='#' data-length={pages.length} onClick={this.handleNext} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.done' defaultMessage='Done' /></a>;
}
const nextOrDoneBtn = hasMore ? (
<button
onClick={this.handleNext}
className='onboarding-modal__nav onboarding-modal__next'
>
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
</button>
) : (
<button
onClick={this.handleClose}
className='onboarding-modal__nav onboarding-modal__done'
>
<FormattedMessage id='onboarding.done' defaultMessage='Done' />
</button>
);
const styles = pages.map((page, i) => ({
key: `page-${i}`,
@@ -243,7 +250,12 @@ class OnboardingModal extends React.PureComponent {
<div className='onboarding-modal__paginator'>
<div>
<a href='#' className='onboarding-modal__skip' onClick={this.handleSkip}><FormattedMessage id='onboarding.skip' defaultMessage='Skip' /></a>
<button
onClick={this.handleSkip}
className='onboarding-modal__nav onboarding-modal__skip'
>
<FormattedMessage id='onboarding.skip' defaultMessage='Skip' />
</button>
</div>
<div className='onboarding-modal__dots'>

View File

@@ -1,20 +1,20 @@
{
"account.block": "مسدودسازی @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.block_domain": "پنهان‌سازی همه چیز از سرور {domain}",
"account.disclaimer": "این کاربر عضو سرور متفاوتی است. شاید عدد واقعی بیشتر از این باشد.",
"account.edit_profile": "ویرایش نمایه",
"account.follow": "پی بگیرید",
"account.followers": "پیگیران",
"account.follows": "پی می‌گیرد",
"account.follows_you": "پیگیر شماست",
"account.media": "Media",
"account.media": "رسانه",
"account.mention": "نام‌بردن از @{name}",
"account.mute": "بی‌صدا کردن @{name}",
"account.posts": "نوشته‌ها",
"account.report": "گزارش @{name}",
"account.requested": "در انتظار پذیرش",
"account.unblock": "رفع انسداد @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_domain": "رفع پنهان‌سازی از {domain}",
"account.unfollow": "پایان پیگیری",
"account.unmute": "باصدا کردن @{name}",
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
@@ -43,8 +43,8 @@
"confirmations.block.message": "آیا واقعاً می‌خواهید {name} را مسدود کنید؟",
"confirmations.delete.confirm": "پاک کن",
"confirmations.delete.message": "آیا واقعاً می‌خواهید این نوشته را پاک کنید؟",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.domain_block.confirm": "پنهان‌سازی کل دامین",
"confirmations.domain_block.message": "آیا جدی جدی می‌خواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقت‌ها مسدودکردن یا بی‌صداکردن چند حساب کاربری خاص کافی است و توصیه می‌شود.",
"confirmations.mute.confirm": "بی‌صدا کن",
"confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟",
"emoji_button.activity": "فعالیت",
@@ -60,18 +60,18 @@
"empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!",
"empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.",
"empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"empty_column.home.inactivity": "فهرست پی‌گیری‌های شما خالی است. اگر مدتی است که غیرفعال بودید، این فهرست به زودی برایتان پر می‌شود.",
"empty_column.home.public_timeline": "فهرست نوشته‌های همه‌جا",
"empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشته‌های دیگران واکنش نشان دهید تا گفتگو آغاز شود.",
"empty_column.public": "این‌جا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا این‌جا پر شود",
"follow_request.authorize": "اجازه دهید",
"follow_request.reject": "اجازه ندهید",
"getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ",
"getting_started.appsshort": "اپ‌ها",
"getting_started.faq": "پرسش‌های رایج",
"getting_started.heading": "آغاز کنید",
"getting_started.open_source_notice": "ماستدون یک نرم‌افزار آزاد است. می‌توانید در ساخت آن مشارکت کنید یا مشکلاتش را در {github} گزارش دهید.",
"getting_started.support": "{faq} • {userguide} • {apps}",
"getting_started.userguide": "User Guide",
"getting_started.userguide": "راهنمای کاربری",
"home.column_settings.advanced": "پیشرفته",
"home.column_settings.basic": "اصلی",
"home.column_settings.filter_regex": "با عبارت‌های باقاعده فیلتر کنید",
@@ -92,10 +92,10 @@
"navigation_bar.mutes": "کاربران بی‌صداشده",
"navigation_bar.preferences": "ترجیحات",
"navigation_bar.public_timeline": "نوشته‌های همه‌جا",
"notification.favourite": "{name} نوشتهٔ شما را پسندید",
"notification.follow": "{name} پیگیر شما شد",
"notification.mention": "{name} از شما نام برد",
"notification.reblog": "{name} نوشتهٔ شما را بازبوقید",
"notification.favourite": "{name} نوشتهٔ شما را پسندید",
"notification.follow": "{name} پیگیر شما شد",
"notification.mention": "{name} از شما نام برد",
"notification.reblog": "{name} نوشتهٔ شما را بازبوقید",
"notifications.clear": "پاک‌کردن اعلان‌ها",
"notifications.clear_confirmation": "واقعاً می‌خواهید همهٔ اعلان‌هایتان را برای همیشه پاک کنید؟",
"notifications.column_settings.alert": "اعلان در کامپیوتر",
@@ -103,8 +103,8 @@
"notifications.column_settings.follow": "پیگیران تازه:",
"notifications.column_settings.mention": "نام‌بردن‌ها:",
"notifications.column_settings.reblog": "بازبوق‌ها:",
"notifications.column_settings.show": "در ستون نشان بده",
"notifications.column_settings.sound": "صدا را پخش کن",
"notifications.column_settings.show": "نمایش در ستون",
"notifications.column_settings.sound": "پخش صدا",
"notifications.settings": "تنظیمات ستون",
"onboarding.done": "پایان",
"onboarding.next": "بعدی",
@@ -114,7 +114,7 @@
"onboarding.page_one.federation": "ماستدون شبکه‌ای از سرورهای مستقل است که با پیوستن به یکدیگر یک شبکهٔ اجتماعی بزرگ را تشکیل می‌دهند.",
"onboarding.page_one.handle": "شما روی سرور {domain} هستید، بنابراین شناسهٔ کامل شما {handle} است.",
"onboarding.page_one.welcome": "به ماستدون خوش آمدید!",
"onboarding.page_six.admin": "نشانی سرور شما {admin} است.",
"onboarding.page_six.admin": "نشانی مسئول سرور شما {admin} است.",
"onboarding.page_six.almost_done": "الان تقریباً آماده‌اید...",
"onboarding.page_six.appetoot": "بوق! بوق!",
"onboarding.page_six.apps_available": "اپ‌های گوناگونی برای اندروید، iOS، و سیستم‌های دیگر موجود است.",
@@ -123,17 +123,17 @@
"onboarding.page_six.read_guidelines": "لطفاً {guidelines} {domain} را بخوانید!",
"onboarding.page_six.various_app": "اپ‌های موبایل",
"onboarding.page_three.profile": "با ویرایش نمایه می‌توانید تصویر نمایه، نوشتهٔ معرفی، و نام نمایشی خود را تغییر دهید. ترجیحات دیگر شما هم آن‌جاست.",
"onboarding.page_three.search": "در نوار جستجو می‌توانید کاربران دیگر را بیابید یا هشتگ‌ها را ببینید، مانند {نقاشی} یا {معرفی}. برای یافتن افرادی که روی سرورهای دیگر هستند، شناسهٔ کامل آن‌ها را بنویسید.",
"onboarding.page_two.compose": "در ستون «نوشتن» می‌توانید نوشته‌های تازه بنویسید. همچنین با دکمه‌های زیر می‌توانید تصویر اضافه کنید، حریم خصوصی نوشته را تنظیم کنید، و هشدار محتوا بگذارید.",
"onboarding.page_three.search": "در نوار جستجو می‌توانید کاربران دیگر را بیابید یا هشتگ‌ها را ببینید، مانند {illustration} یا {introductions}. برای یافتن افرادی که روی سرورهای دیگر هستند، شناسهٔ کامل آن‌ها را بنویسید.",
"onboarding.page_two.compose": "در ستون «نوشتن» می‌توانید نوشته‌های تازه بنویسید. همچنین با دکمه‌های زیرش می‌توانید تصویر اضافه کنید، حریم خصوصی نوشته را تنظیم کنید، و هشدار محتوا بگذارید.",
"onboarding.skip": "رد کن",
"privacy.change": "تنظیم حریم خصوصی نوشته‌ها",
"privacy.direct.long": "تنها به کاربران نام‌برده‌شده نشان بده",
"privacy.direct.short": "مستقیم",
"privacy.private.long": "تنها به پیگیران نشان بده",
"privacy.private.short": "خصوصی",
"privacy.public.long": "در فهرست نوشته‌های عمومی نشان بده",
"privacy.public.long": "در فهرست عمومی نشان بده",
"privacy.public.short": "عمومی",
"privacy.unlisted.long": "در فهرست نوشته‌های همه‌جا نشان نده",
"privacy.unlisted.long": "عمومی، ولی فهرست نکن",
"privacy.unlisted.short": "فهرست‌نشده",
"reply_indicator.cancel": "لغو",
"report.heading": "گزارش تازه",
@@ -144,14 +144,14 @@
"search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}",
"status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
"status.delete": "پاک‌کردن",
"status.favourite": "پسندیدم",
"status.favourite": "پسندیدن",
"status.load_more": "بیشتر نشان بده",
"status.media_hidden": "تصویر پنهان شده",
"status.mention": "نام‌بردن از @{name}",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "بی‌صداکردن گفتگو",
"status.open": "این نوشته را باز کن",
"status.reblog": "بازبوقیدن",
"status.reblogged_by": "{name} بازبوقید",
"status.reblogged_by": "{name} بازبوقید",
"status.reply": "پاسخ",
"status.replyAll": "به نوشته پاسخ دهید",
"status.report": "گزارش دادن @{name}",
@@ -159,7 +159,7 @@
"status.sensitive_warning": "محتوای حساس",
"status.show_less": "نهفتن",
"status.show_more": "نمایش",
"status.unmute_conversation": "Unmute conversation",
"status.unmute_conversation": "باصداکردن گفتگو",
"tabs_bar.compose": "بنویسید",
"tabs_bar.federated_timeline": "همگانی",
"tabs_bar.home": "خانه",

View File

@@ -1,20 +1,20 @@
{
"account.block": "חסימת @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.block_domain": "להסתיר הכל מהקהילה {domain}",
"account.disclaimer": "משתמש זה מגיע מקהילה אחרת. המספר הזה עשוי להיות גדול יותר.",
"account.edit_profile": "עריכת פרופיל",
"account.follow": "מעקב",
"account.followers": "עוקבים",
"account.follows": "נעקבים",
"account.follows_you": "במעקב אחריך",
"account.media": "Media",
"account.media": "מדיה",
"account.mention": "אזכור של @{name}",
"account.mute": "להשתיק את @{name}",
"account.posts": "הודעות",
"account.report": "לדווח על @{name}",
"account.requested": "בהמתנה לאישור",
"account.unblock": "הסרת חסימה מעל @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_domain": "הסר חסימה מקהילת {domain}",
"account.unfollow": "הפסקת מעקב",
"account.unmute": "הפסקת השתקת @{name}",
"boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
@@ -43,8 +43,8 @@
"confirmations.block.message": "לחסום את {name}?",
"confirmations.delete.confirm": "למחוק",
"confirmations.delete.message": "למחוק את ההודעה?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.domain_block.confirm": "הסתר קהילה שלמה",
"confirmations.domain_block.message": "באמת באמת לחסום את כל קהילת {domain}? ברב המקרים השתקות נבחרות של מספר משתמשים מסויימים צריכה להספיק.",
"confirmations.mute.confirm": "להשתיק",
"confirmations.mute.message": "להשתיק את {name}?",
"emoji_button.activity": "פעילות",

View File

@@ -1,61 +1,9 @@
import ar from './ar.json';
import en from './en.json';
import ca from './ca.json';
import de from './de.json';
import es from './es.json';
import fa from './fa.json';
import he from './he.json';
import hr from './hr.json';
import hu from './hu.json';
import io from './io.json';
import it from './it.json';
import fr from './fr.json';
import nl from './nl.json';
import no from './no.json';
import oc from './oc.json';
import pt from './pt.json';
import pt_br from './pt-BR.json';
import uk from './uk.json';
import fi from './fi.json';
import eo from './eo.json';
import ru from './ru.json';
import ja from './ja.json';
import zh_hk from './zh-HK.json';
import zh_cn from './zh-CN.json';
import bg from './bg.json';
import id from './id.json';
import tr from './tr.json';
let theLocale;
const locales = {
ar,
en,
ca,
de,
es,
fa,
he,
hr,
hu,
io,
it,
fr,
nl,
no,
oc,
pt,
'pt-BR': pt_br,
uk,
fi,
eo,
ru,
ja,
'zh-HK': zh_hk,
'zh-CN': zh_cn,
bg,
id,
tr,
};
export function setLocale(locale) {
theLocale = locale;
}
export default function getMessagesForLocale(locale) {
return locales[locale];
};
export function getLocale() {
return theLocale;
}

View File

@@ -1,6 +1,6 @@
{
"account.block": "Blokkeer @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.block_domain": "Negeer alles van {domain}",
"account.disclaimer": "Deze gebruiker zit op een andere server. Dit getal kan hoger zijn.",
"account.edit_profile": "Profiel bewerken",
"account.follow": "Volgen",
@@ -14,9 +14,9 @@
"account.report": "Rapporteer @{name}",
"account.requested": "Wacht op goedkeuring",
"account.unblock": "Deblokkeer @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_domain": "{domain} niet meer negeren",
"account.unfollow": "Ontvolgen",
"account.unmute": "Negeer @{name} niet meer",
"account.unmute": "@{name} niet meer negeren",
"boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
"column.blocks": "Geblokkeerde gebruikers",
"column.community": "Lokale tijdlijn",
@@ -43,8 +43,8 @@
"confirmations.block.message": "Weet je zeker dat je {name} wilt blokkeren?",
"confirmations.delete.confirm": "Verwijderen",
"confirmations.delete.message": "Weet je zeker dat je deze toot wilt verwijderen?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.domain_block.confirm": "Negeer alles van deze server",
"confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
"confirmations.mute.confirm": "Negeren",
"confirmations.mute.message": "Weet je zeker dat je {name} wilt negeren?",
"emoji_button.activity": "Activiteiten",
@@ -148,7 +148,7 @@
"status.load_more": "Meer laden",
"status.media_hidden": "Media verborgen",
"status.mention": "Vermeld @{name}",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "Negeer conversatie",
"status.open": "Toot volledig tonen",
"status.reblog": "Boost",
"status.reblogged_by": "{name} boostte",
@@ -159,7 +159,7 @@
"status.sensitive_warning": "Gevoelige inhoud",
"status.show_less": "Minder tonen",
"status.show_more": "Meer tonen",
"status.unmute_conversation": "Unmute conversation",
"status.unmute_conversation": "Conversatie niet meer negeren",
"tabs_bar.compose": "Schrijven",
"tabs_bar.federated_timeline": "Globaal",
"tabs_bar.home": "Jouw tijdlijn",

View File

@@ -1,6 +1,6 @@
{
"account.block": "Blokkér @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.block_domain": "Skjul alt fra {domain}",
"account.disclaimer": "Denne brukeren er fra en annen instans. Dette tallet kan være høyere.",
"account.edit_profile": "Rediger profil",
"account.follow": "Følg",
@@ -14,7 +14,7 @@
"account.report": "Rapportér @{name}",
"account.requested": "Venter på godkjennelse",
"account.unblock": "Avblokker @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_domain": "Vis {domain}",
"account.unfollow": "Avfølg",
"account.unmute": "Avdemp @{name}",
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
@@ -24,39 +24,39 @@
"column.follow_requests": "Følgeforespørsler",
"column.home": "Hjem",
"column.mutes": "Dempede brukere",
"column.notifications": "Varslinger",
"column.notifications": "Varsler",
"column.public": "Felles tidslinje",
"column_back_button.label": "Tilbake",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked",
"column_subheading.navigation": "Navigasjon",
"column_subheading.settings": "Innstillinger",
"compose_form.lock_disclaimer": "Din konto er ikke {locked}. Hvem som helst kan følge deg og se dine private poster.",
"compose_form.lock_disclaimer.lock": "låst",
"compose_form.placeholder": "Hva har du på hjertet?",
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli fremhevd eller på annen måte bli synlig for uventede mottakere.",
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ikke er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli fremhevd eller på annen måte bli synlig for uventede mottakere.",
"compose_form.publish": "Tut",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Merk media som følsomt",
"compose_form.spoiler": "Skjul tekst bak advarsel",
"compose_form.spoiler_placeholder": "Innholdsadvarsel",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"emoji_button.activity": "Activity",
"emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink",
"confirmation_modal.cancel": "Avbryt",
"confirmations.block.confirm": "Blokkèr",
"confirmations.block.message": "Er du sikker på at du vil blokkere {name}?",
"confirmations.delete.confirm": "Slett",
"confirmations.delete.message": "Er du sikker på at du vil slette denne statusen?",
"confirmations.domain_block.confirm": "Skjul alt fra domenet",
"confirmations.domain_block.message": "Er du sikker på at du vil skjule hele domenet {domain}? I de fleste tilfeller er det bedre med målrettet blokkering eller demping.",
"confirmations.mute.confirm": "Demp",
"confirmations.mute.message": "Er du sikker på at du vil dempe {name}?",
"emoji_button.activity": "Aktivitet",
"emoji_button.flags": "Flagg",
"emoji_button.food": "Mat og drikke",
"emoji_button.label": "Sett inn emoji",
"emoji_button.nature": "Nature",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.search": "Search...",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"emoji_button.nature": "Natur",
"emoji_button.objects": "Objekter",
"emoji_button.people": "Mennesker",
"emoji_button.search": "Søk...",
"emoji_button.symbols": "Symboler",
"emoji_button.travel": "Reise & steder",
"empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
"empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
"empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
@@ -66,13 +66,13 @@
"empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
"follow_request.authorize": "Autorisér",
"follow_request.reject": "Avvis",
"getting_started.appsshort": "Apps",
"getting_started.appsshort": "Apper",
"getting_started.faq": "FAQ",
"getting_started.heading": "Kom i gang",
"getting_started.open_source_notice": "Mastodon er fri programvare. Du kan bidra eller rapportere problemer på GitHub på {github}.",
"getting_started.support": "{faq} • {userguide} • {apps}",
"getting_started.userguide": "User Guide",
"home.column_settings.advanced": "Advansert",
"getting_started.userguide": "Brukerguide",
"home.column_settings.advanced": "Avansert",
"home.column_settings.basic": "Enkel",
"home.column_settings.filter_regex": "Filtrér med regulære uttrykk",
"home.column_settings.show_reblogs": "Vis fremhevinger",
@@ -94,38 +94,38 @@
"navigation_bar.public_timeline": "Felles tidslinje",
"notification.favourite": "{name} likte din status",
"notification.follow": "{name} fulgte deg",
"notification.mention": "{name} mentioned you",
"notification.mention": "{name} nevnte deg",
"notification.reblog": "{name} fremhevde din status",
"notifications.clear": "Fjern varsler",
"notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler?",
"notifications.column_settings.alert": "Skrivebordsvarslinger",
"notifications.column_settings.favourite": "Likt:",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.mention": "Nevninger:",
"notifications.column_settings.reblog": "Fremhevinger:",
"notifications.column_settings.mention": "Nevnt:",
"notifications.column_settings.reblog": "Fremhevet:",
"notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.sound": "Spill lyd",
"notifications.settings": "Kolonneinstillinger",
"onboarding.done": "Done",
"onboarding.next": "Next",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
"onboarding.page_one.welcome": "Welcome to Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobile apps",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
"onboarding.skip": "Skip",
"onboarding.done": "Ferdig",
"onboarding.next": "Neste",
"onboarding.page_five.public_timelines": "Den lokale tidslinjen viser offentlige poster fra alle på {domain}. Felles tidslinje viser offentlige poster fra alle som brukere på {domain} følger. Dette er de offentlige tidslinjene, et fint sted å oppdage nye brukere.",
"onboarding.page_four.home": "Hjem er tidslinjen med alle brukere som du følger.",
"onboarding.page_four.notifications": "Kolonnen med varsler viser når noen interakterer med deg.",
"onboarding.page_one.federation": "Mastdodon er et nettverk med uavhengige servere som sammarbeider om å danne et stort sosialt nettverk. Vi kaller disse serverene instanser.",
"onboarding.page_one.handle": "Du er på {domain}, så ditt fulle brukernavn er {handle}",
"onboarding.page_one.welcome": "Velkommen til Mastodon!",
"onboarding.page_six.admin": "Administratoren på din instans er {admin}.",
"onboarding.page_six.almost_done": "Snart ferdig...",
"onboarding.page_six.appetoot": "Bon Appetut!",
"onboarding.page_six.apps_available": "Det er {apps} tilgjengelig for iOS, Android og andre plattformer.",
"onboarding.page_six.github": "Mastodon er programvare med fri og åpen kildekode. Du kan rapportere feil, be om hjelp eller foreslå endringer på {github}.",
"onboarding.page_six.guidelines": "samfunnets rettningslinjer",
"onboarding.page_six.read_guidelines": "Vennligst les {guidelines} for {domain}!",
"onboarding.page_six.various_app": "mobilapper",
"onboarding.page_three.profile": "Rediger profilen din for å endre din avatar, biografi, og visningsnavn. Der finner du også andre innstillinger.",
"onboarding.page_three.search": "Bruk søkemenyen for å søke etter emneknagger eller brukere, slik som {illustration} og {introductions}. For å søke på en bruker som ikke er på samme instans som deg bruk hele brukernavnet..",
"onboarding.page_two.compose": "Skriv innlegg fra forfatt-kolonnen. Du kan laste opp bilder, justere synlighet, og legge til innholdsvarsler med knappene under.",
"onboarding.skip": "Hopp over",
"privacy.change": "Justér synlighet",
"privacy.direct.long": "Post kun til nevnte brukere",
"privacy.direct.short": "Direkte",
@@ -142,24 +142,24 @@
"report.target": "Rapporterer",
"search.placeholder": "Søk",
"search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}",
"status.cannot_reblog": "This post cannot be boosted",
"status.cannot_reblog": "Denne posten kan ikke fremheves",
"status.delete": "Slett",
"status.favourite": "Lik",
"status.load_more": "Last mer",
"status.media_hidden": "Media skjult",
"status.mention": "Nevn @{name}",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "Demp samtale",
"status.open": "Utvid denne statusen",
"status.reblog": "Fremhev",
"status.reblogged_by": "Fremhevd av {name}",
"status.reply": "Svar",
"status.replyAll": "Reply to thread",
"status.replyAll": "Svar til samtale",
"status.report": "Rapporter @{name}",
"status.sensitive_toggle": "Klikk for å vise",
"status.sensitive_warning": "Følsomt innhold",
"status.show_less": "Vis mindre",
"status.show_more": "Vis mer",
"status.unmute_conversation": "Unmute conversation",
"status.unmute_conversation": "Ikke demp samtale",
"tabs_bar.compose": "Komponer",
"tabs_bar.federated_timeline": "Felles",
"tabs_bar.home": "Hjem",
@@ -172,5 +172,5 @@
"video_player.expand": "Utvid video",
"video_player.toggle_sound": "Veksle lyd",
"video_player.toggle_visible": "Veksle synlighet",
"video_player.video_error": "Video could not be played"
"video_player.video_error": "Video kunne ikke spilles av"
}

View File

@@ -1,25 +1,25 @@
{
"account.block": "Blocar",
"account.block_domain": "Hide everything from {domain}",
"account.block": "Blocar @{name}",
"account.block_domain": "Tot amagar del domeni {domain}",
"account.disclaimer": "Aqueste compte es sus una autra instància. Los nombres pòdon èsser mai grandes.",
"account.edit_profile": "Modificar lo perfil",
"account.follow": "Sègre",
"account.followers": "Abonats",
"account.followers": "Seguidors",
"account.follows": "Abonaments",
"account.follows_you": "Vos sèc",
"account.media": "Media",
"account.mention": "Mencionar",
"account.mute": "Rescondre",
"account.media": "Mèdias",
"account.mention": "Mencionar @{name}",
"account.mute": "Rescondre @{name}",
"account.posts": "Estatuts",
"account.report": "Senhalar",
"account.report": "Senhalar @{name}",
"account.requested": "Invitacion mandada",
"account.unblock": "Desblocar",
"account.unblock_domain": "Unhide {domain}",
"account.unblock": "Desblocar @{name}",
"account.unblock_domain": "Desblocar {domain}",
"account.unfollow": "Quitar de sègre",
"account.unmute": "Quitar de rescondre",
"boost_modal.combo": "Podètz butar {combo} per passar aquò lo còp que ven",
"account.unmute": "Quitar de rescondre @{name}",
"boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
"column.blocks": "Personas blocadas",
"column.community": "Fil public local",
"column.community": "Flux dactualitat public local",
"column.favourites": "Favorits",
"column.follow_requests": "Demandas dabonament",
"column.home": "Acuèlh",
@@ -29,7 +29,7 @@
"column_back_button.label": "Tornar",
"column_subheading.navigation": "Navigacion",
"column_subheading.settings": "Paramètres",
"compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als abonats.",
"compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
"compose_form.lock_disclaimer.lock": "clavat",
"compose_form.placeholder": "A de qué pensatz ?",
"compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz daqueste{domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias a Mastodons. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas dindicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists",
@@ -40,13 +40,13 @@
"compose_form.spoiler_placeholder": "Avertiment",
"confirmation_modal.cancel": "Anullar",
"confirmations.block.confirm": "Blocar",
"confirmations.block.message": "Sètz segur de voler blocar {name}?",
"confirmations.block.message": "Sètz segur de voler blocar {name} ?",
"confirmations.delete.confirm": "Suprimir",
"confirmations.delete.message": "Sètz segur de voler suprimir lestatut ?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.domain_block.confirm": "Amagar tot lo domeni",
"confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
"confirmations.mute.confirm": "Metre en silenci",
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name}?",
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
"emoji_button.activity": "Activitat",
"emoji_button.flags": "Drapèus",
"emoji_button.food": "Manjar e beure",
@@ -57,13 +57,13 @@
"emoji_button.search": "Cercar...",
"emoji_button.symbols": "Simbòls",
"emoji_button.travel": "Viatges & lòcs",
"empty_column.community": "Lo fil public local es void. Escribètz quicòm per lo garnir !",
"empty_column.community": "Lo flux public local es void. Escribètz quicòm per lo garnir !",
"empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
"empty_column.home": "Pel moment segètz pas segun. Visitatz {public} o utilizatz la recèrca per vos connectar a dautras personas.",
"empty_column.home.inactivity": "Vòstra pagina dacuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.",
"empty_column.home.public_timeline": "lo fil public",
"empty_column.home.public_timeline": "lo flux public",
"empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualquun per començar una conversacion.",
"empty_column.public": "I a pas res aquí ! Escribètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo fil public.",
"empty_column.public": "I a pas res aquí ! Escribètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo flux public.",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Regetar",
"getting_started.appsshort": "Apps",
@@ -83,16 +83,16 @@
"media_gallery.toggle_visible": "Modificar la visibilitat",
"missing_indicator.label": "Pas trobat",
"navigation_bar.blocks": "Personas blocadas",
"navigation_bar.community_timeline": "Fil public local",
"navigation_bar.community_timeline": "Flux public local",
"navigation_bar.edit_profile": "Modificar lo perfil",
"navigation_bar.favourites": "Favorits",
"navigation_bar.follow_requests": "Demandas d'abonament",
"navigation_bar.info": "Mai informacions",
"navigation_bar.logout": "Desconnexion",
"navigation_bar.mutes": "Muted users",
"navigation_bar.mutes": "Personas rescondudas",
"navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Fil public global",
"notification.favourite": "{name} a apondut a sos favorits :",
"navigation_bar.public_timeline": "Flux public global",
"notification.favourite": "{name} a ajustat a sos favorits :",
"notification.follow": "{name} vos sèc.",
"notification.mention": "{name} vos a mencionat :",
"notification.reblog": "{name} a partejat vòstre estatut :",
@@ -100,14 +100,14 @@
"notifications.clear_confirmation": "Volètz vertadièrament levar totas vòstras las notificacions ?",
"notifications.column_settings.alert": "Notificacions localas",
"notifications.column_settings.favourite": "Favorits :",
"notifications.column_settings.follow": "Nòus abonats :",
"notifications.column_settings.follow": "Nòus seguidors :",
"notifications.column_settings.mention": "Mencions :",
"notifications.column_settings.reblog": "Partatges :",
"notifications.column_settings.show": "Mostrar dins la colomna",
"notifications.column_settings.sound": "Emetre un son",
"notifications.settings": "Paramètres de la colomna",
"onboarding.done": "Done",
"onboarding.next": "Next",
"onboarding.done": "Fach",
"onboarding.next": "Seguent",
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra intància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
"onboarding.page_four.home": "Lo flux dacuèlh mòstra los estatuts del mond que seguètz.",
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun enteragís amb vos",
@@ -129,11 +129,11 @@
"privacy.change": "Ajustar la confidencialitat del messatge",
"privacy.direct.long": "Mostrar pas qua las personas mencionadas",
"privacy.direct.short": "Dirècte",
"privacy.private.long": "Mostrar pas qua vòstres abonats",
"privacy.private.long": "Mostrar pas qua vòstres seguidors",
"privacy.private.short": "Privat",
"privacy.public.long": "Mostrar dins los fils publics",
"privacy.public.long": "Mostrar dins los fluxes publics",
"privacy.public.short": "Public",
"privacy.unlisted.long": "Mostrar pas dins los fils publics",
"privacy.unlisted.long": "Mostrar pas dins los fluxes publics",
"privacy.unlisted.short": "Pas-listat",
"reply_indicator.cancel": "Anullar",
"report.heading": "Nòu senhalament",
@@ -153,10 +153,10 @@
"status.reblog": "Partejar",
"status.reblogged_by": "{name} a partejat :",
"status.reply": "Respondre",
"status.replyAll": "Reply to thread",
"status.replyAll": "Respondre a la conversacion",
"status.report": "Senhalar @{name}",
"status.sensitive_toggle": "Clicar per mostrar",
"status.sensitive_warning": "Contengut embarrassant",
"status.sensitive_warning": "Contengut sensible",
"status.show_less": "Tornar plegar",
"status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat",
@@ -166,11 +166,11 @@
"tabs_bar.local_timeline": "Flux public local",
"tabs_bar.notifications": "Notificacions",
"upload_area.title": "Lisatz e depausatz per mandar",
"upload_button.label": "Apondre un mèdia",
"upload_button.label": "Ajustar un mèdia",
"upload_form.undo": "Anullar",
"upload_progress.label": "Mandadís…",
"video_player.expand": "Mostrar la vidèo",
"video_player.toggle_sound": "Activar/Desactivar lo son",
"video_player.toggle_visible": "Mostrar/Rescondre la vidèo",
"video_player.video_error": "Video could not be played"
"video_player.video_error": "Fracàs de la lectura de la vidèo"
}

View File

@@ -1,6 +1,6 @@
{
"account.block": "Blokuj @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.block_domain": "Blokuj wszystko z {domain}",
"account.disclaimer": "Ten użytkownik pochodzi z innej instancji. Ta liczba może być większa.",
"account.edit_profile": "Edytuj profil",
"account.follow": "Obserwuj",
@@ -14,7 +14,7 @@
"account.report": "Zgłoś @{name}",
"account.requested": "Oczekująca prośba",
"account.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_domain": "Odblokuj domenę {domain}",
"account.unfollow": "Przestań obserwować",
"account.unmute": "Cofnij wyciszenie @{name}",
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
@@ -43,8 +43,8 @@
"confirmations.block.message": "Czy na pewno chcesz zablokować {name}?",
"confirmations.delete.confirm": "Usuń",
"confirmations.delete.message": "Czy na pewno chcesz usunąć ten status?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.domain_block.confirm": "Ukryj wszysyko z domeny",
"confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.",
"confirmations.mute.confirm": "Wycisz",
"confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?",
"emoji_button.activity": "Aktywność",
@@ -60,18 +60,18 @@
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby odbić piłeczkę!",
"empty_column.hashtag": "Nie ma postów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
"empty_column.home": "Nie obserwujesz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć ciekawych ludzi.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.",
"empty_column.home.public_timeline": "publiczna oś czasu",
"empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
"empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić.",
"follow_request.authorize": "Autoryzuj",
"follow_request.reject": "Odrzuć",
"getting_started.appsshort": "Apps",
"getting_started.appsshort": "Aplikacje",
"getting_started.faq": "FAQ",
"getting_started.heading": "Dowiedz się",
"getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj {github}.",
"getting_started.support": "{faq} • {userguide} • {apps}",
"getting_started.userguide": "User Guide",
"getting_started.userguide": "Podręcznik użytkownika",
"home.column_settings.advanced": "Zaawansowane",
"home.column_settings.basic": "Podstawowe",
"home.column_settings.filter_regex": "Filtruj z użyciem wyrażeń regularnych",
@@ -148,7 +148,7 @@
"status.load_more": "Załaduj więcej",
"status.media_hidden": "Zawartość multimedialna ukryta",
"status.mention": "Wspomnij o @{name}",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "Wycisz konwersację",
"status.open": "Rozszerz ten status",
"status.reblog": "Podbij",
"status.reblogged_by": "{name} podbił",
@@ -159,7 +159,7 @@
"status.sensitive_warning": "Wrażliwa zawartość",
"status.show_less": "Pokaż mniej",
"status.show_more": "Pokaż więcej",
"status.unmute_conversation": "Unmute conversation",
"status.unmute_conversation": "Cofnij wyciezenie konwersacji",
"tabs_bar.compose": "Napisz",
"tabs_bar.federated_timeline": "Globalne",
"tabs_bar.home": "Strona główna",

View File

@@ -1,20 +1,20 @@
{
"account.block": "Блокировать",
"account.block_domain": "Hide everything from {domain}",
"account.block_domain": "Блокировать все с {domain}",
"account.disclaimer": "Это пользователь с другого узла. Число может быть больше.",
"account.edit_profile": "Изменить профиль",
"account.follow": "Подписаться",
"account.followers": "Подписаны",
"account.follows": "Подписки",
"account.follows_you": "Подписан(а) на Вас",
"account.media": "Media",
"account.media": "Медиаконтент",
"account.mention": "Упомянуть",
"account.mute": "Заглушить",
"account.posts": "Посты",
"account.report": "Пожаловаться",
"account.requested": "Ожидает подтверждения",
"account.unblock": "Разблокировать",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_domain": "Разблокировать {domain}",
"account.unfollow": "Отписаться",
"account.unmute": "Снять глушение",
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
@@ -43,8 +43,8 @@
"confirmations.block.message": "Вы уверены, что хотите заблокировать {name}?",
"confirmations.delete.confirm": "Удалить",
"confirmations.delete.message": "Вы уверены, что хотите удалить этот статус?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.domain_block.confirm": "Блокировать весь домен",
"confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.",
"confirmations.mute.confirm": "Заглушить",
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
"emoji_button.activity": "Занятия",
@@ -148,7 +148,7 @@
"status.load_more": "Показать еще",
"status.media_hidden": "Медиаконтент скрыт",
"status.mention": "Упомянуть @{name}",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "Заглушить тред",
"status.open": "Развернуть статус",
"status.reblog": "Продвинуть",
"status.reblogged_by": "{name} продвинул(а)",
@@ -159,7 +159,7 @@
"status.sensitive_warning": "Чувствительный контент",
"status.show_less": "Свернуть",
"status.show_more": "Развернуть",
"status.unmute_conversation": "Unmute conversation",
"status.unmute_conversation": "Снять глушение с треда",
"tabs_bar.compose": "Написать",
"tabs_bar.federated_timeline": "Глобальная",
"tabs_bar.home": "Главная",

View File

@@ -1,20 +1,20 @@
{
"account.block": "Заблокувати",
"account.block_domain": "Hide everything from {domain}",
"account.block_domain": "Заглушити {domain}",
"account.disclaimer": "Це користувач з іншої інстанції. Число може бути більше.",
"account.edit_profile": "Налаштування профілю",
"account.follow": "Підписатися",
"account.followers": "Підписники",
"account.follows": "Підписки",
"account.follows_you": "Підписаний(-а) на Вас",
"account.media": "Media",
"account.media": "Медія",
"account.mention": "Згадати",
"account.mute": "Заглушити",
"account.posts": "Пости",
"account.report": "Поскаржитися",
"account.requested": "Очікує підтвердження",
"account.unblock": "Розблокувати",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_domain": "Розблокувати {domain}",
"account.unfollow": "Відписатися",
"account.unmute": "Зняти глушення",
"boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
@@ -28,8 +28,8 @@
"column.public": "Глобальна стрічка",
"column_back_button.label": "Назад",
"column_subheading.navigation": "Навігація",
"column_subheading.settings": "Settings",
"compose_form.lock_disclaimer": "Ваш акаунт не {locked}. Любой может подписаться на вас и видеть ваши приватные посты.",
"column_subheading.settings": "Налаштування",
"compose_form.lock_disclaimer": "Ваш акаунт не {locked}. Кожен може підписатися на Вас та бачити Ваші приватні пости.",
"compose_form.lock_disclaimer.lock": "приватний",
"compose_form.placeholder": "Що у Вас на думці?",
"compose_form.privacy_disclaimer": "Ваш приватний допис буде доставлено до згаданих користувачів на доменах {domains}. Ви довіряєте {domainsCount, plural, one {цьому серверу} other {цим серверам}}? Приватність постів працює тільки на інстанціях Mastodon. Якщо {domains} {domainsCount, plural, one {не є інстанцією Mastodon} other {не є інстанціями Mastodon}}, приватність поста не буде активована, та він може бути передмухнутий або іншим чином показаний не позначеним Вами користувачам.",
@@ -43,9 +43,9 @@
"confirmations.block.message": "Ви впевнені, що хочете заблокувати {name}?",
"confirmations.delete.confirm": "Видалити",
"confirmations.delete.message": "Ви впевнені, що хочете видалити цей допис?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Заглушить",
"confirmations.domain_block.confirm": "Сховати весь домен",
"confirmations.domain_block.message": "Ви точно, точно впевнені, що хочете заблокувати весь домен {domain}? У більшості випадків для нормальної роботи краще заблокувати/заглушити лише деяких користувачів.",
"confirmations.mute.confirm": "Заглушити",
"confirmations.mute.message": "Ви впевнені, що хочете заглушити {name}?",
"emoji_button.activity": "Заняття",
"emoji_button.flags": "Прапори",
@@ -66,7 +66,7 @@
"empty_column.public": "Тут поки нічого немає! Опублікуйте щось, або вручну підпишіться на користувачів інших інстанцій, щоб заповнити стрічку.",
"follow_request.authorize": "Авторизувати",
"follow_request.reject": "Відмовити",
"getting_started.appsshort": "Apps",
"getting_started.appsshort": "Додатки",
"getting_started.faq": "FAQ",
"getting_started.heading": "Ласкаво просимо",
"getting_started.open_source_notice": "Mastodon - програма з відкритим вихідним кодом. Ви можете допомогти проекту, або повідомити про проблеми на GitHub за адресою {github}.",
@@ -148,7 +148,7 @@
"status.load_more": "Завантажити більше",
"status.media_hidden": "Медіаконтент приховано",
"status.mention": "Згадати",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "Заглушити діалог",
"status.open": "Розгорнути допис",
"status.reblog": "Передмухнути",
"status.reblogged_by": "{name} передмухнув(-ла)",
@@ -159,7 +159,7 @@
"status.sensitive_warning": "Непристойний зміст",
"status.show_less": "Згорнути",
"status.show_more": "Розгорнути",
"status.unmute_conversation": "Unmute conversation",
"status.unmute_conversation": "Зняти глушення з діалогу",
"tabs_bar.compose": "Написати",
"tabs_bar.federated_timeline": "Глобальна",
"tabs_bar.home": "Головна",

View File

@@ -1,7 +1,7 @@
{
"account.block": "屏蔽 @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.disclaimer": "由于这个账户处于另一个服务上,实际数字会比这个更多。",
"account.disclaimer": "由于这个账户处于另一个服务器实例上,实际数字会比这个更多。",
"account.edit_profile": "修改个人资料",
"account.follow": "关注",
"account.followers": "关注者",
@@ -12,7 +12,7 @@
"account.mute": "将 @{name} 静音",
"account.posts": "嘟文",
"account.report": "举报 @{name}",
"account.requested": "等审批",
"account.requested": "等审批",
"account.unblock": "解除对 @{name} 的屏蔽",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "取消关注",
@@ -32,7 +32,7 @@
"compose_form.lock_disclaimer": "你的账户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
"compose_form.lock_disclaimer.lock": "被保护",
"compose_form.placeholder": "在想啥?",
"compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务} other {之中有些不是 Mastodon 服务}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。",
"compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务器实例,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务器实例} other {之中有些不是 Mastodon 服务器实例}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。",
"compose_form.publish": "嘟嘟",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "将媒体文件标示为“敏感内容”",
@@ -63,7 +63,7 @@
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"empty_column.home.public_timeline": "公共时间轴",
"empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
"empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
"empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
"follow_request.authorize": "批准",
"follow_request.reject": "拒绝",
"getting_started.appsshort": "Apps",
@@ -92,29 +92,29 @@
"navigation_bar.mutes": "被静音的用户",
"navigation_bar.preferences": "首选项",
"navigation_bar.public_timeline": "跨站公共时间轴",
"notification.favourite": "{name} 赞你的嘟文",
"notification.favourite": "{name} 赞你的嘟文",
"notification.follow": "{name} 开始关注你",
"notification.mention": "{name} 提及你",
"notification.reblog": "{name} 转嘟你的嘟文",
"notification.reblog": "{name} 转嘟你的嘟文",
"notifications.clear": "清空通知纪录",
"notifications.clear_confirmation": "你确定要清空通知纪录吗?",
"notifications.column_settings.alert": "显示桌面通知",
"notifications.column_settings.favourite": "你的嘟文:",
"notifications.column_settings.favourite": "你的嘟文被赞",
"notifications.column_settings.follow": "关注你:",
"notifications.column_settings.mention": "提及你:",
"notifications.column_settings.reblog": "你的嘟文:",
"notifications.column_settings.reblog": "你的嘟文被转嘟",
"notifications.column_settings.show": "在通知栏显示",
"notifications.column_settings.sound": "播放音效",
"notifications.settings": "字段设置",
"onboarding.done": "出发!",
"onboarding.next": "下一步",
"onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示来自 {domain} 上的各位关注的所有人的公共嘟文。这些就是寻人好去处的公共时间轴啦。",
"onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。",
"onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.",
"onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~",
"onboarding.page_one.federation": "Mastodon 由一系列独立的服务器共同打造一个更大的网络,这就是我们的服务站啦。",
"onboarding.page_one.federation": "Mastodon由一系列独立的服务器共同打造的强大的社交网络,我们将这些独立但又相互连接的服务器叫做服务器实例。",
"onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整账户名称。",
"onboarding.page_one.welcome": "欢迎来到 Mastodon!",
"onboarding.page_six.admin": "{admin} 是你所在服务的管理员.",
"onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
"onboarding.page_six.almost_done": "快完成了...",
"onboarding.page_six.appetoot": "嗷呜~",
"onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~",
@@ -122,9 +122,9 @@
"onboarding.page_six.guidelines": "社区指南",
"onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!",
"onboarding.page_six.various_app": "移动应用程序",
"onboarding.page_three.profile": "编辑你的用户资料,像是头像、简介、和昵称等等。在那还可以找到其它首选项。",
"onboarding.page_three.search": "用搜索来找人和标签吧,像是 {illustration} 或者 {introductions}。想找其它服务上的人,用完整账户名称(用户名@域名)啦。",
"onboarding.page_two.compose": "在这一列开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.",
"onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。",
"onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整账户名称(用户名@域名)啦。",
"onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.",
"onboarding.skip": "好啦好啦我知道啦",
"privacy.change": "调整隐私设置",
"privacy.direct.long": "只有提及的用户能看到",

View File

@@ -1,5 +1,4 @@
// allow override variables here
require.context('../../assets/stylesheets/', false, /variables.*\.scss$/);
const perf = require('./performance');
// import default stylesheet with variables
require('font-awesome/css/font-awesome.css');
@@ -14,21 +13,19 @@ function onDomContentLoaded(callback) {
}
function main() {
perf.start('main()');
const Mastodon = require('mastodon/containers/mastodon').default;
const React = require('react');
const ReactDOM = require('react-dom');
window.Perf = require('react-addons-perf');
require.context('../images/', true);
// import customization styles
require.context('../../assets/stylesheets/', false, /custom.*\.scss$/);
onDomContentLoaded(() => {
const mountNode = document.getElementById('mastodon');
const props = JSON.parse(mountNode.getAttribute('data-props'));
ReactDOM.render(<Mastodon {...props} />, mountNode);
perf.stop('main()');
});
}

View File

@@ -0,0 +1,31 @@
//
// Tools for performance debugging, only enabled in development mode.
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
// Also see config/webpack/loaders/mark.js for the webpack loader marks.
//
let marky;
if (process.env.NODE_ENV === 'development') {
if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {
// Increase Firefox's performance entry limit; otherwise it's capped to 150.
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
performance.setResourceTimingBufferSize(Infinity);
}
marky = require('marky');
// allows us to easily do e.g. ReactPerf.printWasted() while debugging
window.ReactPerf = require('react-addons-perf');
window.ReactPerf.start();
}
export function start(name) {
if (process.env.NODE_ENV === 'development') {
marky.mark(name);
}
}
export function stop(name) {
if (process.env.NODE_ENV === 'development') {
marky.stop(name);
}
}

View File

@@ -91,19 +91,6 @@ const initialState = Immutable.Map({
});
const normalizeStatus = (state, status) => {
const replyToId = status.get('in_reply_to_id');
const id = status.get('id');
if (replyToId) {
if (!state.getIn(['descendants', replyToId], Immutable.List()).includes(id)) {
state = state.updateIn(['descendants', replyToId], Immutable.List(), set => set.push(id));
}
if (!state.getIn(['ancestors', id], Immutable.List()).includes(replyToId)) {
state = state.updateIn(['ancestors', id], Immutable.List(), set => set.push(replyToId));
}
}
return state;
};
@@ -140,7 +127,7 @@ const appendNormalizedTimeline = (state, timeline, statuses, next) => {
return state.updateIn([timeline, 'items'], Immutable.List(), list => list.concat(moreIds));
};
const normalizeAccountTimeline = (state, accountId, statuses, replace = false) => {
const normalizeAccountTimeline = (state, accountId, statuses, replace, next) => {
let ids = Immutable.List();
statuses.forEach((status, i) => {
@@ -151,11 +138,11 @@ const normalizeAccountTimeline = (state, accountId, statuses, replace = false) =
return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map
.set('isLoading', false)
.set('loaded', true)
.set('next', true)
.set('next', next)
.update('items', Immutable.List(), list => (replace ? ids : ids.concat(list))));
};
const normalizeAccountMediaTimeline = (state, accountId, statuses, next) => {
const normalizeAccountMediaTimeline = (state, accountId, statuses, replace, next) => {
let ids = Immutable.List();
statuses.forEach((status, i) => {
@@ -166,7 +153,7 @@ const normalizeAccountMediaTimeline = (state, accountId, statuses, next) => {
return state.updateIn(['accounts_media_timelines', accountId], Immutable.Map(), map => map
.set('isLoading', false)
.set('next', next)
.update('items', Immutable.List(), list => ids.concat(list)));
.update('items', Immutable.List(), list => (replace ? ids : ids.concat(list))));
};
const appendNormalizedAccountTimeline = (state, accountId, statuses, next) => {
@@ -335,7 +322,7 @@ export default function timelines(state = initialState, action) {
case ACCOUNT_TIMELINE_EXPAND_FAIL:
return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false));
case ACCOUNT_TIMELINE_FETCH_SUCCESS:
return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace);
return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace, action.next);
case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next);
case ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST:
@@ -345,7 +332,7 @@ export default function timelines(state = initialState, action) {
case ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL:
return state.updateIn(['accounts_media_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false));
case ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS:
return normalizeAccountMediaTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next);
return normalizeAccountMediaTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace, action.next);
case ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS:
return appendNormalizedAccountMediaTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next);
case ACCOUNT_BLOCK_SUCCESS:

View File

@@ -1,9 +1,30 @@
import main from '../mastodon/main';
if (!window.Intl || !Object.assign || !Number.isNaN ||
!window.Symbol || !Array.prototype.includes) {
// load polyfills dynamically
import('../mastodon/polyfills').then(main).catch(e => {
const needsBasePolyfills = !(
window.Intl &&
Object.assign &&
Number.isNaN &&
window.Symbol &&
Array.prototype.includes
);
const needsExtraPolyfills = !(
window.IntersectionObserver &&
window.requestIdleCallback
);
// Latest version of Firefox and Safari do not have IntersectionObserver.
// Edge does not have requestIdleCallback.
// This avoids shipping them all the polyfills.
if (needsBasePolyfills) {
Promise.all([
import(/* webpackChunkName: "base_polyfills" */ '../mastodon/base_polyfills'),
import(/* webpackChunkName: "extra_polyfills" */ '../mastodon/extra_polyfills'),
]).then(main).catch(e => {
console.error(e); // eslint-disable-line no-console
});
} else if (needsExtraPolyfills) {
import(/* webpackChunkName: "extra_polyfills" */ '../mastodon/extra_polyfills').then(main).catch(e => {
console.error(e); // eslint-disable-line no-console
});
} else {

View File

@@ -1,107 +1,96 @@
import emojify from 'mastodon/emoji';
import { getLocale } from 'mastodon/locales';
import { length } from 'stringz';
import { default as dateFormat } from 'date-fns/format';
import distanceInWordsStrict from 'date-fns/distance_in_words_strict';
import IntlRelativeFormat from 'intl-relativeformat';
import { delegate } from 'rails-ujs';
require.context('../images/', true);
const parseFormat = (format) => format.replace(/%(\w)/g, (_, modifier) => {
switch (modifier) {
case '%':
return '%';
case 'a':
return 'ddd';
case 'A':
return 'ddd';
case 'b':
return 'MMM';
case 'B':
return 'MMMM';
case 'd':
return 'DD';
case 'H':
return 'HH';
case 'I':
return 'hh';
case 'l':
return 'H';
case 'm':
return 'M';
case 'M':
return 'mm';
case 'p':
return 'A';
case 'S':
return 'ss';
case 'w':
return 'd';
case 'y':
return 'YY';
case 'Y':
return 'YYYY';
default:
return `%${modifier}`;
}
});
const { localeData } = getLocale();
localeData.forEach(IntlRelativeFormat.__addLocaleData);
document.addEventListener('DOMContentLoaded', () => {
[].forEach.call(document.querySelectorAll('.emojify'), (content) => {
content.innerHTML = emojify(content.innerHTML);
function main() {
const locale = document.documentElement.lang;
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
const relativeFormat = new IntlRelativeFormat(locale);
[].forEach.call(document.querySelectorAll('time[data-format]'), (content) => {
const format = parseFormat(content.dataset.format);
const formattedDate = dateFormat(content.getAttribute('datetime'), format);
content.textContent = formattedDate;
});
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const timeAgo = distanceInWordsStrict(new Date(), content.getAttribute('datetime'), {
addSuffix: true,
document.addEventListener('DOMContentLoaded', () => {
[].forEach.call(document.querySelectorAll('.emojify'), (content) => {
content.innerHTML = emojify(content.innerHTML);
});
[].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
const formattedDate = dateTimeFormat.format(datetime);
content.title = formattedDate;
content.textContent = formattedDate;
});
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
content.textContent = relativeFormat.format(datetime);;
});
content.textContent = timeAgo;
});
});
delegate(document, '.video-player video', 'click', ({ target }) => {
if (target.paused) {
target.play();
} else {
target.pause();
}
});
delegate(document, '.video-player video', 'click', ({ target }) => {
if (target.paused) {
target.play();
} else {
target.pause();
}
});
delegate(document, '.media-spoiler', 'click', ({ target }) => {
target.style.display = 'none';
});
delegate(document, '.media-spoiler', 'click', ({ target }) => {
target.style.display = 'none';
});
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
if (button !== 0) {
return true;
}
window.location.href = target.href;
return false;
});
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
if (button !== 0) {
return true;
}
window.location.href = target.href;
return false;
});
delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
const contentEl = target.parentNode.parentNode.querySelector('.e-content');
if (contentEl.style.display === 'block') {
contentEl.style.display = 'none';
target.parentNode.style.marginBottom = 0;
} else {
contentEl.style.display = 'block';
target.parentNode.style.marginBottom = null;
}
return false;
});
delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
const contentEl = target.parentNode.parentNode.querySelector('.e-content');
if (contentEl.style.display === 'block') {
contentEl.style.display = 'none';
target.parentNode.style.marginBottom = 0;
} else {
contentEl.style.display = 'block';
target.parentNode.style.marginBottom = null;
}
return false;
});
delegate(document, '.account_display_name', 'input', ({ target }) => {
const nameCounter = document.querySelector('.name-counter');
nameCounter.textContent = 30 - length(target.value);
});
delegate(document, '.account_display_name', 'input', ({ target }) => {
const nameCounter = document.querySelector('.name-counter');
if (nameCounter) {
nameCounter.textContent = 30 - length(target.value);
}
});
delegate(document, '.account_note', 'input', ({ target }) => {
const noteCounter = document.querySelector('.note-counter');
noteCounter.textContent = 160 - length(target.value);
});
delegate(document, '.account_note', 'input', ({ target }) => {
const noteCounter = document.querySelector('.note-counter');
if (noteCounter) {
noteCounter.textContent = 160 - length(target.value);
}
});
}
if (!window.Intl) {
import(/* webpackChunkName: "base_polyfills" */ 'mastodon/base_polyfills').then(() => {
main();
}).catch(error => {
console.log(error); // eslint-disable-line no-console
});
} else {
main();
}

View File

@@ -1,6 +1,7 @@
.card {
background: $ui-base-color;
background-size: cover;
background-position: center;
padding: 60px 0;
padding-bottom: 0;
border-radius: 4px 4px 0 0;

View File

@@ -245,4 +245,19 @@
.report-status__actions {
flex: 0 0 auto;
position: relative;
.nsfw-button {
color: $white;
font-size: 11px;
width: 11px;
display: block;
}
.trash-button {
position: absolute;
bottom: 10px;
font-size: 24px;
width: 24px;
}
}

View File

@@ -45,8 +45,13 @@ body {
}
}
button:focus {
outline: none;
button {
font-family: inherit;
cursor: pointer;
&:focus {
outline: none;
}
}
.app-holder {

View File

@@ -60,6 +60,7 @@
.column-collapsable {
position: relative;
flex: 0 0 auto;
.column-collapsable__content {
overflow: auto;
@@ -554,13 +555,17 @@
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
@keyframes fade {
0% { opacity: 0; }
100% { opacity: 1; }
}
opacity: 1;
animation: fade 150ms linear;
&.status-direct {
background: lighten($ui-base-color, 8%);
.status__display-name strong {
color: $primary-text-color;
}
.icon-button.disabled {
color: lighten($ui-base-color, 16%);
}
@@ -604,6 +609,16 @@
}
}
.notification-favourite {
.status.status-direct {
background: transparent;
.icon-button.disabled {
color: lighten($ui-base-color, 13%);
}
}
}
.status__relative-time {
color: lighten($ui-base-color, 26%);
float: right;
@@ -662,7 +677,7 @@
margin-top: 10px;
}
.status__action-bar-button-wrapper {
.status__action-bar-button {
float: left;
margin-right: 18px;
}
@@ -1311,7 +1326,9 @@
.drawer {
flex: 1 1 100%;
overflow: hidden;
contain: strict;
@supports(display: grid) { // hack to fix Chrome <57
contain: strict;
}
}
@media screen and (min-width: 360px) {
@@ -1486,7 +1503,9 @@
flex: 1 1 auto;
backface-visibility: hidden;
-webkit-overflow-scrolling: touch;
contain: strict;
@supports(display: grid) { // hack to fix Chrome <57
contain: strict;
}
&.optionally-scrollable {
overflow-y: auto;
@@ -1778,6 +1797,7 @@
.getting-started__wrapper {
position: relative;
flex: 0 0 auto;
}
.getting-started__footer {
@@ -1945,9 +1965,15 @@ button.icon-button.active i.fa-retweet {
.load-more {
display: block;
color: lighten($ui-base-color, 26%);
background-color: transparent;
border: 0;
font-size: inherit;
text-align: center;
line-height: inherit;
margin: 0;
padding: 15px;
text-decoration: none;
width: 100%;
clear: both;
&:hover {
background: lighten($ui-base-color, 2%);
@@ -2233,7 +2259,9 @@ button.icon-button.active i.fa-retweet {
flex: 1 1 auto;
align-items: center;
justify-content: center;
contain: strict;
@supports(display: grid) { // hack to fix Chrome <57
contain: strict;
}
a {
color: $ui-highlight-color;
@@ -2893,11 +2921,15 @@ button.icon-button.active i.fa-retweet {
min-width: 33px;
}
a {
.onboarding-modal__nav {
color: darken($ui-secondary-color, 34%);
text-decoration: none;
background-color: transparent;
border: 0;
font-size: 14px;
font-weight: 500;
padding: 0;
line-height: inherit;
height: auto;
&:hover,
&:focus,
@@ -2981,13 +3013,14 @@ button.icon-button.active i.fa-retweet {
.onboarding-modal__page-one {
display: flex;
align-items: center;
}
.onboarding-modal__page-one__elephant-friend {
background: url('../images/elephant-friend.png') no-repeat center center / contain;
width: 147px;
height: 160px;
margin-right: 10px;
background: url('../images/elephant-friend-1.png') no-repeat center center / contain;
width: 155px;
height: 193px;
margin-right: 15px;
}
@media screen and (max-width: 400px) {
@@ -3145,6 +3178,7 @@ button.icon-button.active i.fa-retweet {
.boost-modal__action-bar,
.confirmation-modal__action-bar {
display: flex;
justify-content: space-between;
background: $ui-secondary-color;
padding: 10px;
line-height: 36px;
@@ -3179,14 +3213,9 @@ button.icon-button.active i.fa-retweet {
}
.confirmation-modal__action-bar {
& > div {
text-align: left;
padding: 0 16px;
}
a {
.confirmation-modal__cancel-button {
background-color: transparent;
color: darken($ui-secondary-color, 34%);
text-decoration: none;
font-size: 14px;
font-weight: 500;
@@ -3366,7 +3395,7 @@ button.icon-button.active i.fa-retweet {
object-fit: cover;
position: relative;
top: 50%;
transform: translateY(-35%);
transform: translateY(-50%);
width: 100%;
z-index: 1;
}

View File

@@ -66,7 +66,7 @@ body.rtl {
float: left;
}
.status__action-bar-button-wrapper {
.status__action-bar-button {
float: right;
margin-right: 0;
margin-left: 18px;

View File

@@ -56,4 +56,8 @@ module Extractor
tags.each { |tag| yield tag[:hashtag], tag[:indices].first, tag[:indices].last } if block_given?
tags
end
def extract_cashtags_with_indices(_text)
[] # always returns empty array
end
end

View File

@@ -2,10 +2,8 @@
module Settings
module Extend
extend ActiveSupport::Concern
def settings
ScopedSettings.for_thing(self)
@settings ||= ScopedSettings.new(self)
end
end
end

View File

@@ -1,14 +1,68 @@
# frozen_string_literal: true
module Settings
class ScopedSettings < ::Setting
def self.for_thing(object)
class ScopedSettings
def initialize(object)
@object = object
self
end
def self.thing_scoped
unscoped.where(thing_type: @object.class.base_class.to_s, thing_id: @object.id)
# rubocop:disable Style/MethodMissing
def method_missing(method, *args)
method_name = method.to_s
# set a value for a variable
if method_name[-1] == '='
var_name = method_name.sub('=', '')
value = args.first
self[var_name] = value
else
# retrieve a value
self[method_name]
end
end
# rubocop:enable Style/MethodMissing
def respond_to_missing?(*)
true
end
def all_as_records
vars = thing_scoped
records = vars.map { |r| [r.var, r] }.to_h
Setting.default_settings.each do |key, default_value|
next if records.key?(key) || default_value.is_a?(Hash)
records[key] = Setting.new(var: key, value: default_value)
end
records
end
def []=(key, value)
key = key.to_s
record = thing_scoped.find_or_initialize_by(var: key)
record.update!(value: value)
Rails.cache.write(Setting.cache_key(key, @object), value)
value
end
def [](key)
Rails.cache.fetch(Setting.cache_key(key, @object)) do
db_val = thing_scoped.find_by(var: key.to_s)
if db_val
default_value = Setting.default_settings[key]
return default_value.with_indifferent_access.merge!(db_val.value) if default_value.is_a?(Hash)
db_val.value
else
Setting.default_settings[key]
end
end
end
protected
def thing_scoped
Setting.unscoped.where(thing_type: @object.class.base_class.to_s, thing_id: @object.id)
end
end
end

View File

@@ -89,6 +89,8 @@ class Account < ApplicationRecord
scope :recent, -> { reorder(id: :desc) }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
delegate :email,
:current_sign_in_ip,

View File

@@ -18,8 +18,6 @@ class AccountFilter
private
def scope_for(key, value)
accounts = Account.arel_table
case key.to_s
when 'local'
Account.local
@@ -34,21 +32,26 @@ class AccountFilter
when 'suspended'
Account.suspended
when 'username'
Account.where(accounts[:username].matches("#{value}%"))
Account.matches_username(value)
when 'display_name'
Account.where(accounts[:display_name].matches("#{value}%"))
Account.matches_display_name(value)
when 'email'
users = User.arel_table
Account.joins(:user).merge(User.where(users[:email].matches("#{value}%")))
accounts_with_users.merge User.matches_email(value)
when 'ip'
return Account.default_scoped unless valid_ip?(value)
matches_ip = User.where(current_sign_in_ip: value).or(User.where(last_sign_in_ip: value))
Account.joins(:user).merge(matches_ip)
if valid_ip?(value)
accounts_with_users.merge User.with_recent_ip_address(value)
else
Account.default_scoped
end
else
raise "Unknown filter: #{key}"
end
end
def accounts_with_users
Account.joins(:user)
end
def valid_ip?(value)
IPAddr.new(value)
true

View File

@@ -50,6 +50,8 @@ module AccountInteractions
# Mute relationships
has_many :mute_relationships, class_name: 'Mute', foreign_key: 'account_id', dependent: :destroy
has_many :muting, -> { order('mutes.id desc') }, through: :mute_relationships, source: :target_account
has_many :muted_by_relationships, class_name: 'Mute', foreign_key: :target_account_id, dependent: :destroy
has_many :muted_by, -> { order('mutes.id desc') }, through: :muted_by_relationships, source: :account
has_many :conversation_mutes, dependent: :destroy
has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy
end

View File

@@ -23,4 +23,6 @@ class Follow < ApplicationRecord
has_one :notification, as: :activity, dependent: :destroy
validates :account_id, uniqueness: { scope: :target_account_id }
scope :recent, -> { reorder(id: :desc) }
end

View File

@@ -55,6 +55,7 @@ class MediaAttachment < ApplicationRecord
validates :account, presence: true
scope :attached, -> { where.not(status_id: nil) }
scope :unattached, -> { where(status_id: nil) }
scope :local, -> { where(remote_url: '') }
default_scope { order(id: :asc) }

View File

@@ -49,7 +49,7 @@ class Notification < ApplicationRecord
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
def activity(eager_loaded = true)
eager_loaded ? send(activity_type.downcase) : super
eager_loaded ? send(activity_type.underscore) : super()
end
def type

View File

@@ -23,7 +23,7 @@ class Setting < RailsSettings::Base
def [](key)
return super(key) unless rails_initialized?
val = Rails.cache.fetch(cache_key(key, @object)) do
val = Rails.cache.fetch(cache_key(key, nil)) do
db_val = object(key)
if db_val
@@ -35,7 +35,6 @@ class Setting < RailsSettings::Base
default_settings[key]
end
end
val
end
@@ -51,8 +50,6 @@ class Setting < RailsSettings::Base
records
end
private
def default_settings
return {} unless RailsSettings::Default.enabled?
RailsSettings::Default.instance

View File

@@ -55,8 +55,9 @@ class Status < ApplicationRecord
validates_with StatusLengthValidator
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
default_scope { order(id: :desc) }
default_scope { recent }
scope :recent, -> { reorder(id: :desc) }
scope :remote, -> { where.not(uri: nil) }
scope :local, -> { where(uri: nil) }

View File

@@ -25,6 +25,7 @@ class StreamEntry < ApplicationRecord
STATUS_INCLUDES = [:account, :stream_entry, :conversation, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :conversation, :media_attachments, :tags, mentions: :account], thread: [:stream_entry, :account]].freeze
default_scope { where(activity_type: 'Status') }
scope :recent, -> { reorder(id: :desc) }
scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) }
delegate :target, :title, :content, :thread,

View File

@@ -35,6 +35,7 @@
class User < ApplicationRecord
include Settings::Extend
ACTIVE_DURATION = 14.days
devise :registerable, :recoverable,
:rememberable, :trackable, :validatable, :confirmable,
@@ -51,6 +52,9 @@ class User < ApplicationRecord
scope :recent, -> { order(id: :desc) }
scope :admins, -> { where(admin: true) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
scope :with_recent_ip_address, ->(value) { where(arel_table[:current_sign_in_ip].eq(value).or(arel_table[:last_sign_in_ip].eq(value))) }
before_validation :sanitize_languages

View File

@@ -59,7 +59,13 @@ class AccountSearchService < BaseService
end
def exact_match
@_exact_match ||= Account.find_remote(query_username, query_domain)
@_exact_match ||= begin
if domain_is_local?
Account.find_local(query_username)
else
Account.find_remote(query_username, query_domain)
end
end
end
def search_results

View File

@@ -2,6 +2,8 @@
module AuthorExtractor
def author_from_xml(xml)
return nil if xml.nil?
# Try <email> for acct
acct = xml.at_xpath('./xmlns:author/xmlns:email', xmlns: TagManager::XMLNS)&.content

View File

@@ -21,7 +21,7 @@ class NotifyService < BaseService
end
def blocked_favourite?
false
@recipient.muting?(@notification.from_account)
end
def blocked_follow?

View File

@@ -47,25 +47,30 @@ class ProcessFeedService < BaseService
return
end
status, just_created = nil
Rails.logger.debug "Creating remote status #{id}"
status, just_created = status_from_xml(@xml)
return if status.nil?
return status unless just_created
ApplicationRecord.transaction do
status, just_created = status_from_xml(@xml)
if verb == :share
original_status = shared_status_from_xml(@xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS))
status.reblog = original_status
return if status.nil?
return status unless just_created
if original_status.nil?
status.destroy
return nil
elsif original_status.reblog?
status.reblog = original_status.reblog
if verb == :share
original_status = shared_status_from_xml(@xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS))
status.reblog = original_status
if original_status.nil?
status.destroy
return nil
elsif original_status.reblog?
status.reblog = original_status.reblog
end
end
end
status.save!
status.save!
end
notify_about_mentions!(status) unless status.reblog?
notify_about_reblog!(status) if status.reblog? && status.reblog.account.local?
@@ -184,7 +189,7 @@ class ProcessFeedService < BaseService
def find_status(uri)
if TagManager.instance.local_id?(uri)
local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Status')
return Status.find(local_id)
return Status.find_by(id: local_id)
end
Status.find_by(uri: uri)

View File

@@ -108,12 +108,18 @@ class ProcessInteractionService < BaseService
def favourite!(xml, from_account)
current_status = status(xml)
return if current_status.nil?
favourite = current_status.favourites.where(account: from_account).first_or_create!(account: from_account)
NotifyService.new.call(current_status.account, favourite)
end
def unfavourite!(xml, from_account)
current_status = status(xml)
return if current_status.nil?
favourite = current_status.favourites.where(account: from_account).first
favourite&.destroy
end

View File

@@ -24,7 +24,10 @@
.activity-stream.activity-stream-headless
.entry= render partial: 'stream_entries/simple_status', locals: { status: status }
.report-status__actions
= link_to admin_report_reported_status_path(@report, status), method: :delete, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: t('admin.reports.delete') do
- unless status.media_attachments.empty?
= link_to admin_report_reported_status_path(@report, status, status: { sensitive: !status.sensitive }), method: :patch, class: 'nsfw-button', title: t("admin.reports.nsfw.#{!status.sensitive}") do
= t("admin.reports.nsfw.#{!status.sensitive}")
= link_to admin_report_reported_status_path(@report, status), method: :delete, class: 'icon-button trash-button', title: t('admin.reports.delete'), data: { confirm: t('admin.reports.are_you_sure') } do
= fa_icon 'trash'
- if !@report.action_taken?

View File

@@ -1,5 +1,7 @@
object @media
attribute :id, :type
node(:url) { |media| full_asset_url(media.file.url(:original)) }
node(:url) { |media| full_asset_url(media.file.url(:original)) }
node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
node(:text_url) { |media| medium_url(media) }
node(:text_url) { |media| medium_url(media) }
node(:meta) { |media| media.file.meta }

View File

@@ -3,4 +3,4 @@ attributes :id, :remote_url, :type
node(:url) { |media| full_asset_url(media.file.url(:original)) }
node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
node(:text_url) { |media| media.local? ? medium_url(media) : nil }
node(:meta) { |media| media.file.meta }
node(:meta) { |media| media.file.meta }

View File

@@ -1,4 +1,6 @@
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitive, :spoiler_text, :visibility
attributes :id, :created_at, :in_reply_to_id,
:in_reply_to_account_id, :sensitive,
:spoiler_text, :visibility, :language
node(:uri) { |status| TagManager.instance.uri_for(status) }
node(:content) { |status| Formatter.instance.format(status) }

View File

@@ -18,8 +18,10 @@
= ' - '
= title
= stylesheet_pack_tag 'application', media: 'all'
= stylesheet_pack_tag 'common', media: 'all'
= stylesheet_pack_tag stylesheet_for_layout, media: 'all'
= javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
= javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
= csrf_meta_tags
= yield :header_tags

View File

@@ -7,7 +7,7 @@
- if !user_signed_in? && single_user_mode?
%span.single-user-login
= link_to t('auth.login'), new_user_session_path
\u2014
&mdash;
%span.domain= link_to site_hostname, root_path
%span.powered-by
!= t('generic.powered_by', link: link_to('Mastodon', 'https://github.com/tootsuite/mastodon'))

Some files were not shown because too many files have changed in this diff Show More