Compare commits

..

123 Commits

Author SHA1 Message Date
kibigo!
bc8532359b Mastodon GO! -> v0.1.1 2017-11-06 20:00:03 -08:00
kibigo!
e0298d66f8 Autocollapse boosts option 2017-11-05 15:05:12 -08:00
beatrix
73bf0ea7d1 Merge pull request #204 from glitch-soc/with-mastodon-go
Introducing: Mastodon GO!
2017-11-04 09:14:08 -04:00
kibigo!
276432790a Introducing: Mastodon GO! 2017-11-04 05:48:42 -07:00
beatrix
254b74c71f add memorial to production.rb
in memory of Natalie Nguyen

let her name ring through the ether
2017-11-03 12:34:50 -04:00
David Yip
870d71b78b Merge branch 'master' into gs-master 2017-10-27 09:45:25 -05:00
nullkal
781105293c Feature: Unlisted custom emojis (#5485) 2017-10-27 16:11:30 +02:00
puckipedia
0cb329f63a Allow ActivityPub Note's tag and attachment to be single objects (#5534) 2017-10-27 16:10:36 +02:00
unarist
0129f5eada Optimize FixReblogsInFeeds migration (#5538)
We have changed how we store reblogs in the redis for bigint IDs. This process is done by 1) scan all entries in users feed, and 2) re-store reblogs by 3 write commands.

However, this operation is really slow for large instances. e.g. 1hrs on friends.nico (w/ 50k users). So I have tried below tweaks.

* It checked non-reblogs by `entry[0] == entry[1]`, but this condition won't work because `entry[0]` is String while `entry[1]` is Float. Changing `entry[0].to_i == entry[1]` seems work.
  -> about 4-20x faster (feed with less reblogs will be faster)
* Write operations can be batched by pipeline
  -> about 6x faster
* Wrap operation by Lua script and execute by EVALSHA command. This really reduces packets between Ruby and Redis.
  -> about 3x faster

I've taken Lua script way, though doing other optimizations may be enough.
2017-10-27 16:10:22 +02:00
Jenkins
656f5b6f87 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-10-26 15:08:34 +00:00
erin
22da775a85 Fix copying emojos: redirect to the page you were on (#5509) 2017-10-26 23:44:24 +09:00
David Yip
dd28b94cf0 Merge remote-tracking branch 'origin/master' into gs-master 2017-10-26 09:18:27 -05:00
りんすき
d556be2968 Fix column design broken with very long title (#5493)
* Fix #5314

* fix not beautiful code

* fix broken design with mobile view

* remove no longer needed code
2017-10-26 22:52:48 +09:00
unarist
4f337c020a Fix Cocaine::ExitStatusError when upload small non-animated GIF (#5489)
Looks like copied tempfile need to be flushed before further processing. This issue won't happen if the uploaded file has enough file size.
2017-10-26 22:48:35 +09:00
Nolan Lawson
02f7f3619a Remove translateZ(0) on modal overlay (#5478) 2017-10-26 22:46:50 +09:00
beatrix
a2612d0d38 Merge pull request #179 from glitch-soc/keyword-mute
Keyword muting
2017-10-25 17:37:48 -04:00
beatrix
31814ddda0 Merge pull request #198 from glitch-soc/gs-direct-timeline
Direct messages timeline from tootsuite/mastodon#4514
2017-10-25 17:37:06 -04:00
David Yip
42f2045c21 Merge remote-tracking branch 'STJrInuyasha/feature/direct-timeline' into gs-direct-timeline 2017-10-25 16:01:20 -05:00
Jenkins
5f0268ab84 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-10-25 16:17:14 +00:00
Ratmir Karabut
20fee786b1 Update Russian translation (#5517)
* 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

* Update Russian translation (pin)

* Update Russian translation (account deletion)

* Fix extra line

* Update Russian translation (sessions)

* Update Russian translation

* Update Russian translation

* Fix merge conflicts (revert)

* Update Russian translation

* Update Russian translation (fix)

* Update Russian translation (fix quotes)

* Update Russian translation (fix quotes)

* Update Russian translation (fix)

* Update Russian translation

* Add quotes

* bundle exec i18n-tasks normalize
2017-10-26 00:21:58 +09:00
Anna e só
74777599cf l10n: PT-BR translation updated (#5530) 2017-10-25 23:11:03 +09:00
Olivier Nicole
1ba3725473 Complete Esperanto translation (#5520) 2017-10-25 22:38:37 +09:00
David Yip
e40fe4092d Remove nil check in Glitch::KeywordMute#=~.
@regex can no longer be nil, so we don't need to check it.
2017-10-24 19:03:59 -05:00
David Yip
d9485e6497 Assume Glitch::KeywordMute#destroy! works and error out if it doesn't.
There's nothing useful we can display if the destroy action messes up,
so might as well assert it does and complain loudly if it doesn't.
2017-10-24 18:56:57 -05:00
David Yip
d5c8ebe205 Use edit template for displaying errors in update. 2017-10-24 18:56:44 -05:00
David Yip
d03b48cea0 Also filter notifications containing muted keywords. 2017-10-24 18:51:27 -05:00
David Yip
9226257a1b Override Action View name inference in settings/keyword_mutes.
Glitch::KeywordMute's name is inferred as glitch_keyword_mutes, and in
templates this turns into e.g. settings/glitch/keyword_mutes.  Going
along with this convention means a lot of file movement, though, and for
a UI that's as temporary and awkward as this one I think it's less
effort to slap a bunch of as: options everywhere.

We'll do the Right Thing when we build out the API and frontend UI.
2017-10-24 18:40:28 -05:00
David Yip
641f90e73a Fix example description.
This example actually checks matches at the end of a string.
2017-10-24 18:33:02 -05:00
David Yip
f5a3283976 Switch to Regexp.union for building the mute expression.
Also make the keyword-building methods private: they always probably
should have been private, but now I have encoded enough fun and games
into them that it now seems wrong for them to *not* be private.
2017-10-24 18:31:34 -05:00
Ondřej Hruška
516eeeb43d option to add title to <Button>, use for toot buttons (#197) 2017-10-24 19:08:07 +02:00
David Yip
664c9aa708 Merge pull request #196 from glitch-soc/fix-imports
Added app/javascript for imports
2017-10-23 23:34:43 -05:00
kibigo!
119d477c8b Added app/javascript for imports 2017-10-23 20:22:48 -07:00
David Yip
8410d33b49 Only cache the regex text, not the regex itself.
It is possible to cache a Regexp object, but I'm not sure what happens
if e.g. that object remains in cache across two different Ruby versions.
Caching a string seems to raise fewer questions.
2017-10-23 19:31:59 -05:00
David Yip
4f01e6e8d5 Merge remote-tracking branch 'origin/master' into gs-master 2017-10-22 22:57:41 -05:00
Matthew Walsh
a76b024228 Changes to match other timelines in 2.0 2017-10-22 18:45:35 -07:00
Matthew Walsh
3db80f75a6 Added a timeline for Direct statuses
* Lists all Direct statuses you've sent and received
* Displayed in Getting Started
* Streaming server support for direct TL
2017-10-22 18:35:14 -07:00
David Yip
af8f06413e KeywordMute matcher: more closely mimic Regexp#=~ behavior.
Regexp#=~ returns nil if it does not match.  An empty mute set does not
match any status, so KeywordMute::Matcher#=~ ought to return nil also.
2017-10-22 01:12:21 -05:00
David Yip
1a60445a5f Address unused translation errors. 2017-10-22 01:05:56 -05:00
David Yip
4c84513e04 Use current_account from ApplicationController.
This avoids copy-pasting definitions of set_account.
2017-10-22 01:02:52 -05:00
David Yip
4b68e82a19 Don't add \b to whole-word keywords that don't start with word characters.
Ditto for ending with \b.

Consider muting the phrase "(hot take)".  I stipulate it is reasonable
to enter this with the default "match whole word" behavior.  Under the
old behavior, this would be encoded as

    \b\(hot\ take\)\b

However, if \b is before the first character in the string and the first
character in the string is not a word character, then the match will
fail.  Ditto for after.  In our example, "(" is not a word character, so
this will not match statuses containing "(hot take)", and that's a very
surprising behavior.

To address this, we only add leading and trailing \b to keywords that
start or end with word characters.
2017-10-22 00:38:54 -05:00
David Yip
19826774f0 keyword mutes: also check spoiler (CW) text and reblogged statuses. 2017-10-22 00:38:53 -05:00
Marcin Mikołajczak
fdb0848e08 i18n: Update Polish Translation (#5494) 2017-10-22 08:34:39 +09:00
David Yip
ad86c86fa8 Apply keyword mutes to reblogs. 2017-10-21 15:44:47 -05:00
David Yip
670e6a33f8 Move KeywordMute into Glitch namespace.
There are two motivations for this:

1. It looks like we're going to add other features that require
   server-side storage (e.g. user notes).

2. Namespacing glitchsoc modifications is a good idea anyway: even if we
   do not end up doing (1), if upstream introduces a keyword-mute feature
   that also uses a "KeywordMute" model, we can avoid some merge
   conflicts this way and work on the more interesting task of
   choosing which implementation to use.
2017-10-21 14:54:36 -05:00
David Yip
cd04e3df58 Fill in create, edit, update, and destroy for keyword mutes interface.
Also add a destroy-all action, which can be useful if you're flushing an
old list entirely to start a new one.
2017-10-21 14:54:36 -05:00
David Yip
4a64181461 Allow keywords to match either substrings or whole words.
Word-boundary matching only works as intended in English and languages
that use similar word-breaking characters; it doesn't work so well in
(say) Japanese, Chinese, or Thai.  It's unacceptable to have a feature
that doesn't work as intended for some languages.  (Moreso especially
considering that it's likely that the largest contingent on the Mastodon
bit of the fediverse speaks Japanese.)

There are rules specified in Unicode TR29[1] for word-breaking across
all languages supported by Unicode, but the rules deliberately do not
cover all cases.  In fact, TR29 states

    For example, reliable detection of word boundaries in languages such
    as Thai, Lao, Chinese, or Japanese requires the use of dictionary
    lookup, analogous to English hyphenation.

So we aren't going to be able to make word detection work with regexes
within Mastodon (or glitchsoc).  However, for a first pass (even if it's
kind of punting) we can allow the user to choose whether they want word
or substring detection and warn about the limitations of this
implementation in, say, docs.

[1]: https://unicode.org/reports/tr29/
     https://web.archive.org/web/20171001005125/https://unicode.org/reports/tr29/
2017-10-21 14:54:36 -05:00
David Yip
2e03a10059 Spike out index and new views for keyword mutes controller. 2017-10-21 14:54:36 -05:00
David Yip
4fa2f7e82d Set up /settings/keyword_mutes. #164.
This should eventually be accessible via the API and the web frontend,
but I find it easier to set up an editing interface using Rails
templates and the like.  We can always take it out if it turns out we
don't need it.
2017-10-21 14:54:36 -05:00
David Yip
b4b657eb1d Invalidate cached matcher objects on KeywordMute commit. #164. 2017-10-21 14:54:36 -05:00
David Yip
693c66dfde Use more idiomatic string concatentation. #164.
The intent of the previous concatenation was to minimize object
allocations, which can end up being a slow killer.  However, it turns
out that under MRI 2.4.x, the shove-strings-in-an-array-and-join method
is not only arguably more common but (in this particular case) actually
allocates *fewer* objects than the string concatenation.

Or, at least, that's what I gather by running this:

    words = %w(palmettoes nudged hibernation bullish stockade's tightened Hades
    Dixie's formalize superego's commissaries Zappa's viceroy's apothecaries
    tablespoonful's barons Chennai tollgate ticked expands)

    a = Account.first

    KeywordMute.transaction do
      words.each { |w| KeywordMute.create!(keyword: w, account: a) }

      GC.start

      s1 = GC.stat

      re = String.new.tap do |str|
        scoped = KeywordMute.where(account: a)
        keywords = scoped.select(:id, :keyword)
        count = scoped.count

        keywords.find_each.with_index do |kw, index|
          str << Regexp.escape(kw.keyword.strip)
          str << '|' if index < count - 1
        end
      end

      s2 = GC.stat

      puts s1.inspect, s2.inspect

      raise ActiveRecord::Rollback
    end

vs this:

    words = %w( palmettoes nudged hibernation bullish stockade's tightened Hades Dixie's
    formalize superego's commissaries Zappa's viceroy's apothecaries tablespoonful's
    barons Chennai tollgate ticked expands
    )

    a = Account.first

    KeywordMute.transaction do
      words.each { |w| KeywordMute.create!(keyword: w, account: a) }

      GC.start

      s1 = GC.stat

      re = [].tap do |arr|
        KeywordMute.where(account: a).select(:keyword, :id).find_each do |m|
          arr << Regexp.escape(m.keyword.strip)
        end
      end.join('|')

      s2 = GC.stat

      puts s1.inspect, s2.inspect

      raise ActiveRecord::Rollback
    end

Using rails r, here is a comparison of the total_allocated_objects and
malloc_increase_bytes GC stat data:

                 total_allocated_objects        malloc_increase_bytes
string concat    3200241 -> 3201428 (+1187)     1176 -> 45216 (44040)
array join       3200380 -> 3201299 (+919)      1176 -> 36448 (35272)
2017-10-21 14:54:36 -05:00
David Yip
a4851100fd Make use of the regex attr_reader. #164.
It would also have been valid to get rid of the attr_reader, but I like
being able to reach inside KeywordMute::Matcher without resorting to
instance_variable_get tomfoolery.
2017-10-21 14:54:36 -05:00
David Yip
9f609bc94e Fix case-insensitive match scenario; test some word ornamentation. #164. 2017-10-21 14:54:36 -05:00
David Yip
603cf02b70 Rework KeywordMute interface to use a matcher object; spec out matcher. #164.
A matcher object that builds a match from KeywordMute data and runs it
over text is, in my view, one of the easier ways to write examples for
this sort of thing.
2017-10-21 14:54:36 -05:00
David Yip
4745d6eeca Spec out KeywordMute interface. #164. 2017-10-21 14:54:21 -05:00
David Yip
9093e2de7a Add KeywordMute model.
Gist of the proposed keyword mute implementation:

Keyword mutes are represented server-side as one keyword per record.
For each account, there exists a keyword regex that is generated as one
big alternation of all keywords.  This regex is cached (in Redis, I
guess) so we can quickly get it when filtering in FeedManager.
2017-10-21 14:53:41 -05:00
Ondřej Hruška
d589dd7cd0 Compose buttons bar redesign + generalize dropdown (#194)
* Generalize compose dropdown for re-use

* wip stuffs

* new tootbox look and removed old doodle button files

* use the house icon for ...
2017-10-21 20:24:53 +02:00
beatrix
a7be86e875 hide mentions of muted accounts (in home col) (#190)
* hide mentions of muted accounts (in home col)

also cleans up some old crap

* add test
2017-10-20 10:49:54 -04:00
beatrix
b15dd05514 Merge pull request #191 from glitch-soc/garglamel-yaml
ƔAML update
2017-10-19 19:29:52 -04:00
kibigo!
21bafc6555 Updates to bio metadata script 2017-10-19 16:11:53 -07:00
beatrix
f5e2469485 Merge pull request #189 from glitch-soc/scrollable-compose-area
Make the compose area optionally scrollable
2017-10-19 12:28:47 -04:00
Nolan Lawson
8392ddbf87 Remove unnecessary translateZ(0) when doing scale() (#5473) 2017-10-19 18:27:55 +02:00
David Yip
9423553e5c Make the compose area optionally scrollable.
On desktop, the compose text box grows to accommodate the content.  On
mobile, the text box does not grow to accommodate text context, but does
grow to accommodate images.  It is possible in both cases to overflow
the available area, which makes accessing other UI elements (e.g.
visibility setttings) difficult.

This commit makes the compose area optionally scrollable, which allows
those UI elements to remain available even if they go off-screen.
2017-10-19 10:59:50 -05:00
masarakki
049381b284 remove-duplicated-jest-config (#5465) 2017-10-19 13:51:38 +02:00
David Yip
90770f6d59 Merge pull request #185 from glitch-soc/fix-null-status
workaround for null status
2017-10-18 17:01:31 -05:00
beatrix
c756651278 Merge pull request #188 from glitch-soc/merge-upstream-2-0-0
Merge with upstream v2.0.0
2017-10-18 17:57:42 -04:00
Ondřej Hruška
eb907a5bab formatting fix for eslint 2017-10-18 23:44:06 +02:00
David Yip
39c9cdf7fe Remove unused filesAttached property. 2017-10-18 13:20:45 -05:00
David Yip
86cf4468af Update stylesheet imports in glitch components.
Commit 6e54719474 moved the Mastodon
variables and mixins deeper in the directory hierarchy; this commit
brings the glitch components in line with that change.
2017-10-18 11:52:34 -05:00
David Yip
42e8c8eb0e Merge tag 'v2.0.0' into gs-master 2017-10-18 11:52:04 -05:00
Sho Kusano
09d81defcd Suppress type error(not a function) on calling fastSeek (#5452) 2017-10-18 17:13:51 +02:00
Eugen Rochko
3810d98cd8 Bump version to 2.0.0 🐘 2017-10-18 13:53:56 +02:00
Renato "Lond" Cerqueira
26b2a6a71e Fix pt-BR translation strings related to advanced search. (#5449) 2017-10-18 13:53:17 +02:00
Technowix
edf9a5e4fc Revert #5438 for FR (#5450)
As said here https://github.com/tootsuite/mastodon/pull/5438 the point of shortening the timestamp is legit, and after some time of adaptation no mistakes can be mades.
2017-10-18 13:51:30 +02:00
Håkan Eriksson
c710069c12 Some typos and supplementation in sentence structures (#5441)
* Swedish file added

* Swedish file added

* Swedish file updated

* Swedish languagefile added

* Add Swedish translation

* Add Swedish translation

* Started the Swedish translation

* Added Swedish lang settings

* Updating Swedish language

* Updating Swedish language

* Updating Swedish language

* Updating Swedish language

* Updating Swedish language

* Updating Swedish language

* Swedish language completed and added

* Swedish language Simple_form added

* Swedish language Divise added

* Swedish language doorkeeper added

* Swedish language - now all file complete

* Swedish - Typos and supplementation in sentence structure

* Update simple_form.sv.yml

* Update sv.yml

* Update sv.yml

Rearranged the alphabetical order.
2017-10-18 13:50:52 +02:00
Yamagishi Kazutoshi
990d6dd565 Run i18n-tasks checked-normalized in Travis CI (#5443) 2017-10-18 11:57:02 +02:00
Yamagishi Kazutoshi
402da46ff6 Enable coverage for Jest (#5442) 2017-10-18 11:39:36 +02:00
JohnD28
e7099d8d9e fr.json typo (realtive time) (#5447)
Typo correction : https://fr.wikipedia.org/wiki/Heure#Typographie
2017-10-18 16:47:14 +09:00
Eugen Rochko
637ea3bb5b Bump version to 2.0.0rc4 2017-10-17 23:16:35 +02:00
Jeroen
363d0d3a44 Dutch: A few strings I found last minute (#5440)
* Dutch: A few strings I found last minute

* Update simple_form.nl.yml
2017-10-17 23:15:40 +02:00
YaQ
6e54719474 Make theme directory and Rename application.scss (#5336)
* Rename application.scss

* Move to 'default' directory

* Follow review

Move directory, and Rename "mastodon".

* Revert rename

* undo removal of newline
2017-10-18 05:20:07 +09:00
Eugen Rochko
f3003417c5 When unreblog arrives over streaming API, just delete in UI (#5439) 2017-10-17 22:17:02 +02:00
JeanGauthier
33ea042dec i18n better wording (OC/CA/FR) + string added in FR (#5438)
* Update oc.json

* Update fr.json

* Update ca.json

* Update fr.json

* Update simple_form.fr.yml

* Update ca.json

* Update fr.json

* Update oc.json

* Update oc.json

* Update fr.json
2017-10-17 21:49:39 +02:00
Nolan Lawson
8b22a63ab0 Fix scrolling of dropdown when offscreen (#5434) 2017-10-17 20:38:16 +02:00
Eugen Rochko
05686cc99d Fix unwanted content warning gap in CSS (#5436)
Fix #5356
2017-10-17 20:07:44 +02:00
Eugen Rochko
484208ce12 When status is fetched instead of delivered, do not stream it (#5437) 2017-10-17 20:05:21 +02:00
unarist
3bc8924940 Don't capture scheme-less URLs in the status (#5435)
Specifically, this fixes status length calculation to be same as JS side.

BTW, since this pattern used in not only preview card fetching, we
should extract it (with twitter-regex?) and write tests I think.
2017-10-17 18:32:25 +02:00
JeanGauthier
a02de9e012 i18n update (#5427) in OC/CA/FR/ES (#5431)
* Update oc.json

* Update ca.json

* Update fr.json

* Update es.json
2017-10-17 13:03:28 +02:00
unarist
2d395324e1 Fix React warning about tabIndex on status with CW (#5432) 2017-10-17 13:03:12 +02:00
Jakob Kramer
e6c9756fa9 Update German translation (#5425) 2017-10-17 12:02:47 +02:00
Yamagishi Kazutoshi
5050719fac Add Japanese translations (#5427)
* yarn manage:translations

* Add Japanese translation for #5410

* Add Japanese translation for #5393
2017-10-17 11:47:05 +02:00
Yamagishi Kazutoshi
989553c69a Normalize locale files (#5429) 2017-10-17 11:45:59 +02:00
masarakki
0e0c6b1b4b use-DB_NAME-in-development (#5430) 2017-10-17 11:45:37 +02:00
aschmitz
554c2fd8af Clean up reblog tracking keys, related improvements (#5428)
* Clean up reblog-tracking sets from FeedManager

Builds on #5419, with a few minor optimizations and cleanup of sets
after they are no longer needed.

* Update tests, fix multiply-reblogged case

Previously, we would have lost the fact that a given status was
reblogged if the displayed reblog of it was removed, now we don't.

Also added tests to make sure FeedManager#trim cleans up our reblog
tracking keys, fixed up FeedCleanupScheduler to use the right loop,
and fixed the test for it.
2017-10-17 11:45:06 +02:00
Håkan Eriksson
a2b600428c Swedish translation (#5406)
* Swedish file added

* Swedish file added

* Swedish file updated

* Swedish languagefile added

* Add Swedish translation

* Add Swedish translation

* Started the Swedish translation

* Added Swedish lang settings

* Updating Swedish language

* Updating Swedish language

* Updating Swedish language

* Updating Swedish language

* Updating Swedish language

* Updating Swedish language

* Swedish language completed and added

* Swedish language Simple_form added

* Swedish language Divise added

* Swedish language doorkeeper added

* Swedish language - now all file complete
2017-10-17 13:41:36 +09:00
Jeroen
df1a9c5ab5 Small update Dutch (nl) strings (#5424)
* Update Dutch strings

* Update Dutch
2017-10-17 01:00:23 +02:00
Ondřej Hruška
4421f6598f Fix dual toot button wrong behavior after compose re-design 2017-10-16 23:32:13 +02:00
Ondřej Hruška
64a5e9a1de workaround for null status 2017-10-16 23:09:39 +02:00
beatrix
7364b26e4b Merge pull request #184 from tootsuite/master
Fix reduced motion breaking public galleries (#5423)
2017-10-16 17:05:42 -04:00
David Yip
313ba202ef Merge pull request #182 from glitch-soc/wide-tootbox-panel
Full-width tootbox panel to make room for buttons
2017-10-16 15:49:22 -05:00
David Yip
7c44ad6355 Merge pull request #181 from glitch-soc/upstream-merge-again
Merge upstream, pull in fixes for tootsuite/mastodon#{5409,5417}
2017-10-16 15:46:12 -05:00
Ondřej Hruška
37ff061d9b satisfy eslint and jest 2017-10-16 22:24:44 +02:00
Ondřej Hruška
8b9ee5f16b Merge remote-tracking branch 'upstream/master' into upstream-merge-again 2017-10-16 21:13:47 +02:00
Ondřej Hruška
4b397adb5b fix some lint errors 2017-10-16 21:13:34 +02:00
Nolan Lawson
8980aa804f Fix reduced motion breaking public galleries (#5423) 2017-10-16 21:08:01 +02:00
Eugen Rochko
34118169ac Keep references to all reblogs of a status on home feed (#5419)
* Keep references to all reblogs of a status on home feed

When inserting reblog: Add to set of reblogs of this status on
the feed, if original status was present in the feed, add it to
that set as well.

When removing a reblog: Remove it from that set. Take random
remaining item from the set. If one exists, re-insert it into feed,
otherwise do not re-insert anything.

Fix #4210

* When original is removed, toss out reblog references
2017-10-16 20:44:31 +02:00
Daigo 3 Dango
4fd7aebd5e Fix typo in a db:rollback script (#5422)
Reported at
https://don.inux39.me/@inux39/1406082
https://don.inux39.me/@inux39/1406134
2017-10-16 20:29:49 +02:00
David Yip
65154869df Merge remote-tracking branch 'upstream/master' into upstream-merge-again 2017-10-16 09:54:16 -05:00
Eugen Rochko
bc89995f65 Bump version to 2.0.0rc3 2017-10-16 16:29:00 +02:00
David Yip
7e9d93472c Merge remote-tracking branch 'upstream/master' into gs-master 2017-10-16 09:27:01 -05:00
David Yip
dbb1fce94d Merge remote-tracking branch 'upstream/master' into gs-master 2017-10-16 09:23:59 -05:00
Eugen Rochko
7cc71748ce Ensure that feed renegeration restores non-zero items (#5409)
Fix #5398

Ordering the home timeline query by account_id meant that the first
100 items belonged to a single account. There was also no reason to
reverse-iterate over the statuses. Assuming the user accesses the
feed halfway-through, it's better to have recent statuses already
available at the top. Therefore working from newer->older is ideal.

If the algorithm ends up filtering all items out during last-mile
filtering, repeat again a page further. The algorithm terminates
when either at least one item has been added, or if the database
query returns nothing (end of data reached)
2017-10-16 16:08:51 +02:00
Eugen Rochko
aec70b44fc Filter out duplicate IDs in timelines reducer (#5417)
Possibly the cause of #5379, #5377
2017-10-16 15:59:30 +02:00
unarist
6f490b4bfe Fix un-reblogged status being at wrong position in the home timeline (#5418)
We've changed un-reblogging behavior when we implement Snowflake, to insert un-reblogged status at the position reblogging status existed.

However, our API expects home timeline is ordered by status ids, and max_id/since_id filters by zset score. Due to this, un-reblogged status appears as a last item of result set, and timeline expansion may skips many statuses.

So this reverts that change...reblogged status inserted at corresponding position to its id.
2017-10-16 15:58:23 +02:00
voidSatisfaction
03975dbde4 Add up-to-date korean translation on client (#5402)
* chore: add Korean translation for client

* fix: change unlisted and embed Korean words
2017-10-16 14:39:28 +02:00
KY
f72936b4e6 Fix #5082 - disable retweet link for followers only toot (#5397)
* Fix #5082 - disable retweet link for followers only toot

* Hide reblog count when it is a direct message
2017-10-16 14:10:12 +02:00
Marcin Mikołajczak
3c530d95f6 i18n: Update Polish translation (#5416)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-10-16 14:09:51 +02:00
JeanGauthier
1e7b3bf625 i18n ultim hour «More» dropdown title + reduce motion (#5415)
* Correction fem. form

* More dropdown title

* More dropdown title

* More dropdown title

* Add option to reduce motion (#5393)
2017-10-16 14:09:26 +02:00
Yamagishi Kazutoshi
bf0ee1a25c Enable ESLint rules import/* (#5414)
* Enable ESLint rules import/*

* fix
2017-10-16 11:12:09 +02:00
Nolan Lawson
fa0be3f834 Add option to reduce motion (#5393)
* Add option to reduce motion

* Use HOC to wrap all Motion calls

* fix case-sensitive issue

* Avoid updating too frequently

* Get rid of unnecessary change to _simple_status.html.haml
2017-10-16 09:36:15 +02:00
Nolan Lawson
981e20b03a Fix offline-plugin warning in dev mode (#5411) 2017-10-16 09:33:50 +02:00
Yamagishi Kazutoshi
d5b767c374 Replace JavaScript Testing Framework from Mocha to Jest (#5412) 2017-10-16 09:33:08 +02:00
Nolan Lawson
93b54b8d4b i18n "More" dropdown title (#5410) 2017-10-16 09:31:47 +02:00
Nolan Lawson
e7ab9bf8b4 Refactor and simplify icon_button.js (#5413) 2017-10-16 09:30:09 +02:00
231 changed files with 4842 additions and 1647 deletions

View File

@@ -5,12 +5,14 @@ env:
browser: true
node: true
es6: true
jest: true
parser: babel-eslint
plugins:
- react
- jsx-a11y
- import
parserOptions:
sourceType: module
@@ -21,8 +23,19 @@ parserOptions:
modules: true
spread: true
rules:
settings:
import/extensions:
- .js
import/ignore:
- node_modules
- \\.(css|scss|json)$
import/resolver:
node:
moduleDirectory:
- node_modules
- app/javascript
rules:
brace-style: warn
comma-dangle:
- error
@@ -125,3 +138,17 @@ rules:
jsx-a11y/role-supports-aria-props: off
jsx-a11y/scope: warn
jsx-a11y/tabindex-no-positive: warn
import/extensions:
- error
- always
- js: never
import/newline-after-import: error
import/no-extraneous-dependencies:
- error
- devDependencies:
- "config/webpack/**"
- "app/javascript/mastodon/test_setup.js"
- "app/javascript/**/__tests__/**"
import/no-unresolved: error
import/no-webpack-loader-syntax: error

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "app/javascript/themes/mastodon-go"]
path = app/javascript/themes/mastodon-go
url = https://github.com/marrus-sh/mastodon-go

View File

@@ -53,5 +53,5 @@ before_script:
script:
- travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec
- npm test
- bundle exec i18n-tasks unused
- yarn test
- bundle exec i18n-tasks check-normalized && bundle exec i18n-tasks unused

2
Vagrantfile vendored
View File

@@ -83,7 +83,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb|
vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "2048"]
vb.customize ["modifyvm", :id, "--memory", "4096"]
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
# https://github.com/mitchellh/vagrant/issues/1172

View File

@@ -22,6 +22,14 @@ module Admin
end
end
def update
if @custom_emoji.update(resource_params)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
else
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
end
end
def destroy
@custom_emoji.destroy
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
@@ -36,7 +44,7 @@ module Admin
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
end
redirect_to admin_custom_emojis_path(params[:page])
redirect_to admin_custom_emojis_path(page: params[:page])
end
def enable
@@ -56,7 +64,7 @@ module Admin
end
def resource_params
params.require(:custom_emoji).permit(:shortcode, :image)
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end
def filtered_custom_emojis

View File

@@ -0,0 +1,60 @@
# frozen_string_literal: true
class Api::V1::Timelines::DirectController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:show]
before_action :require_user!, only: [:show]
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
private
def load_statuses
cached_direct_statuses
end
def cached_direct_statuses
cache_collection direct_statuses, Status
end
def direct_statuses
direct_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def direct_timeline_statuses
Status.as_direct_timeline(current_account)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def next_path
api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@@ -0,0 +1,64 @@
# frozen_string_literal: true
class Settings::KeywordMutesController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :load_keyword_mute, only: [:edit, :update, :destroy]
def index
@keyword_mutes = paginated_keyword_mutes_for_account
end
def new
@keyword_mute = keyword_mutes_for_account.build
end
def create
@keyword_mute = keyword_mutes_for_account.create(keyword_mute_params)
if @keyword_mute.persisted?
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
else
render :new
end
end
def update
if @keyword_mute.update(keyword_mute_params)
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
else
render :edit
end
end
def destroy
@keyword_mute.destroy!
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
end
def destroy_all
keyword_mutes_for_account.delete_all
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
end
private
def keyword_mutes_for_account
Glitch::KeywordMute.where(account: current_account)
end
def load_keyword_mute
@keyword_mute = keyword_mutes_for_account.find(params[:id])
end
def keyword_mute_params
params.require(:keyword_mute).permit(:keyword, :whole_word)
end
def paginated_keyword_mutes_for_account
keyword_mutes_for_account.order(:keyword).page params[:page]
end
end

View File

@@ -39,6 +39,7 @@ class Settings::PreferencesController < ApplicationController
:setting_boost_modal,
:setting_delete_modal,
:setting_auto_play_gif,
:setting_reduce_motion,
:setting_system_font_ui,
:setting_noindex,
:setting_theme,

View File

@@ -9,6 +9,10 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value
end
def as_array(value)
value.is_a?(Array) ? value : [value]
end
def value_or_id(value)
value.is_a?(String) || value.nil? ? value : value['id']
end

View File

@@ -0,0 +1,2 @@
module Settings::KeywordMutesHelper
end

View File

@@ -27,6 +27,7 @@ module SettingsHelper
pt: 'Português',
'pt-BR': 'Português do Brasil',
ru: 'Русский',
sv: 'Svenska',
th: 'ภาษาไทย',
tr: 'Türkçe',
uk: 'Українська',

View File

@@ -48,7 +48,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports //
import emojify from 'mastodon/features/emoji/emoji';
import emojify from '../../../mastodon/features/emoji/emoji';
import IconButton from '../../../mastodon/components/icon_button';
import Avatar from '../../../mastodon/components/avatar';

View File

@@ -47,11 +47,9 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl';
// Mastodon imports //
import IconButton from '../../../../mastodon/components/icon_button';
// Our imports //
import ComposeAdvancedOptionsToggle from './toggle';
import ComposeDropdown from '../dropdown/index';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
@@ -77,11 +75,6 @@ const messages = defineMessages({
{ id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' },
});
const iconStyle = {
height : null,
lineHeight : '27px',
};
/*
Implementation:
@@ -100,67 +93,6 @@ export default class ComposeAdvancedOptions extends React.PureComponent {
intl : PropTypes.object.isRequired,
};
state = {
open: false,
};
/*
### `onToggleDropdown()`
This function toggles the opening and closing of the advanced options
dropdown.
*/
onToggleDropdown = () => {
this.setState({ open: !this.state.open });
};
/*
### `onGlobalClick(e)`
This function closes the advanced options dropdown if you click
anywhere else on the screen.
*/
onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false });
}
}
/*
### `componentDidMount()`, `componentWillUnmount()`
This function closes the advanced options dropdown if you click
anywhere else on the screen.
*/
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick);
}
/*
### `setRef(c)`
`setRef()` stores a reference to the dropdown's `<div> in `this.node`.
*/
setRef = (c) => {
this.node = c;
}
/*
@@ -171,7 +103,6 @@ anywhere else on the screen.
*/
render () {
const { open } = this.state;
const { intl, values } = this.props;
/*
@@ -218,23 +149,14 @@ toggle as its `key` so that React can keep track of it.
Finally, we can render our component.
*/
return (
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}>
<div className='advanced-options-dropdown__value'>
<IconButton
className='advanced-options-dropdown__value'
title={intl.formatMessage(messages.advanced_options_icon_title)}
icon='ellipsis-h' active={open || anyEnabled}
size={18}
style={iconStyle}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{optionElems}
</div>
</div>
<ComposeDropdown
title={intl.formatMessage(messages.advanced_options_icon_title)}
icon='home'
highlight={anyEnabled}
>
{optionElems}
</ComposeDropdown>
);
}

View File

@@ -0,0 +1,133 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, defineMessages } from 'react-intl';
// Our imports //
import ComposeDropdown from '../dropdown/index';
import { uploadCompose } from '../../../../mastodon/actions/compose';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { openModal } from '../../../../mastodon/actions/modal';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({
upload :
{ id: 'compose.attach.upload', defaultMessage: 'Upload a file' },
doodle :
{ id: 'compose.attach.doodle', defaultMessage: 'Draw something' },
attach :
{ id: 'compose.attach', defaultMessage: 'Attach...' },
});
const mapStateToProps = state => ({
// This horrible expression is copied from vanilla upload_button_container
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
resetFileKey: state.getIn(['compose', 'resetFileKey']),
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
});
const mapDispatchToProps = dispatch => ({
onSelectFile (files) {
dispatch(uploadCompose(files));
},
onOpenDoodle () {
dispatch(openModal('DOODLE', { noEsc: true }));
},
});
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
export default class ComposeAttachOptions extends ImmutablePureComponent {
static propTypes = {
intl : PropTypes.object.isRequired,
resetFileKey: PropTypes.number,
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
disabled: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired,
onOpenDoodle: PropTypes.func.isRequired,
};
handleItemClick = bt => {
if (bt === 'upload') {
this.fileElement.click();
}
if (bt === 'doodle') {
this.props.onOpenDoodle();
}
this.dropdown.setState({ open: false });
};
handleFileChange = (e) => {
if (e.target.files.length > 0) {
this.props.onSelectFile(e.target.files);
}
}
setFileRef = (c) => {
this.fileElement = c;
}
setDropdownRef = (c) => {
this.dropdown = c;
}
render () {
const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
const options = [
{ icon: 'cloud-upload', text: messages.upload, name: 'upload' },
{ icon: 'paint-brush', text: messages.doodle, name: 'doodle' },
];
const optionElems = options.map((item) => {
const hdl = () => this.handleItemClick(item.name);
return (
<div
role='button'
tabIndex='0'
key={item.name}
onClick={hdl}
className='privacy-dropdown__option'
>
<div className='privacy-dropdown__option__icon'>
<i className={`fa fa-fw fa-${item.icon}`} />
</div>
<div className='privacy-dropdown__option__content'>
<strong>{intl.formatMessage(item.text)}</strong>
</div>
</div>
);
});
return (
<div>
<ComposeDropdown
title={intl.formatMessage(messages.attach)}
icon='paperclip'
disabled={disabled}
ref={this.setDropdownRef}
>
{optionElems}
</ComposeDropdown>
<input
key={resetFileKey}
ref={this.setFileRef}
type='file'
multiple={false}
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleFileChange}
disabled={disabled}
style={{ display: 'none' }}
/>
</div>
);
}
}

View File

@@ -0,0 +1,77 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
// Mastodon imports //
import IconButton from '../../../../mastodon/components/icon_button';
const iconStyle = {
height : null,
lineHeight : '27px',
};
export default class ComposeDropdown extends React.PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
icon: PropTypes.string,
highlight: PropTypes.bool,
disabled: PropTypes.bool,
children: PropTypes.arrayOf(PropTypes.node).isRequired,
};
state = {
open: false,
};
onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false });
}
};
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick);
}
onToggleDropdown = () => {
if (this.props.disabled) return;
this.setState({ open: !this.state.open });
};
setRef = (c) => {
this.node = c;
};
render () {
const { open } = this.state;
let { highlight, title, icon, disabled } = this.props;
if (!icon) icon = 'ellipsis-h';
return (
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${highlight ? 'active' : ''} `}>
<div className='advanced-options-dropdown__value'>
<IconButton
className={'inverted'}
title={title}
icon={icon} active={open || highlight}
size={18}
style={iconStyle}
disabled={disabled}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{this.props.children}
</div>
</div>
);
}
}

View File

@@ -2,10 +2,10 @@
import { connect } from 'react-redux';
// Mastodon imports //
import { closeModal } from 'mastodon/actions/modal';
import { closeModal } from '../../../mastodon/actions/modal';
// Our imports //
import { changeLocalSetting } from 'glitch/actions/local_settings';
import { changeLocalSetting } from '../../../glitch/actions/local_settings';
import LocalSettings from '.';
const mapStateToProps = state => ({

View File

@@ -8,7 +8,7 @@ import LocalSettingsPage from './page';
import LocalSettingsNavigation from './navigation';
// Stylesheet imports
import './style';
import './style.scss';
export default class LocalSettings extends React.PureComponent {

View File

@@ -7,7 +7,7 @@ import { injectIntl, defineMessages } from 'react-intl';
import LocalSettingsNavigationItem from './item';
// Stylesheet imports
import './style';
import './style.scss';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
// Stylesheet imports
import './style';
import './style.scss';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

View File

@@ -1,4 +1,4 @@
@import 'styles/variables';
@import 'styles/mastodon/variables';
.glitch.local-settings__navigation__item {
display: block;

View File

@@ -1,4 +1,4 @@
@import 'styles/variables';
@import 'styles/mastodon/variables';
.glitch.local-settings__navigation {
background: $primary-text-color;

View File

@@ -8,7 +8,7 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import LocalSettingsPageItem from './item';
// Stylesheet imports
import './style';
import './style.scss';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
@@ -124,6 +124,16 @@ export default class LocalSettingsPage extends React.PureComponent {
>
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'reblogs']}
id='mastodon-settings--collapsed-auto-reblogs'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_reblogs' defaultMessage='Boosts' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'replies']}

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
// Stylesheet imports
import './style';
import './style.scss';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

View File

@@ -1,4 +1,4 @@
@import 'styles/variables';
@import 'styles/mastodon/variables';
.glitch.local-settings__page__item {
select {

View File

@@ -1,4 +1,4 @@
@import 'styles/variables';
@import 'styles/mastodon/variables';
.glitch.local-settings__page {
display: block;

View File

@@ -1,4 +1,4 @@
@import 'styles/variables';
@import 'styles/mastodon/variables';
.glitch.local-settings {
position: relative;

View File

@@ -102,6 +102,16 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, ownProps) => {
let status = getStatus(state, ownProps.id);
if(status === null) {
console.error(`ERROR! NULL STATUS! ${ownProps.id}`);
// work-around: find first good status
for (let k of state.get('statuses').keys()) {
status = getStatus(state, k);
if (status !== null) break;
}
}
let reblogStatus = status.get('reblog', null);
let account = undefined;
let prepend = undefined;

View File

@@ -287,6 +287,7 @@ properly and our intersection observer is good to go.
muted,
id,
intersectionObserverWrapper,
prepend,
} = this.props;
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
@@ -299,6 +300,9 @@ properly and our intersection observer is good to go.
node.clientHeight > (
status.get('media_attachments').size && !muted ? 650 : 400
)
) || (
autoCollapseSettings.get('reblogs') &&
prepend === 'reblogged_by'
) || (
autoCollapseSettings.get('replies') &&
status.get('in_reply_to_id', null) !== null

View File

@@ -14,6 +14,7 @@
"settings.auto_collapse_lengthy": "Lengthy toots",
"settings.auto_collapse_media": "Toots with media",
"settings.auto_collapse_notifications": "Notifications",
"settings.auto_collapse_reblogs": "Boosts",
"settings.auto_collapse_replies": "Replies",
"settings.close": "Close",
"settings.collapsed_statuses": "Collapsed toots",

View File

@@ -59,6 +59,7 @@ const initialState = ImmutableMap({
all : false,
notifications : true,
lengthy : true,
reblogs : false,
replies : false,
media : false,
}),

View File

@@ -69,6 +69,10 @@ functions are:
easier to read and to maintain. I leave it to the future readers of
this code to determine the extent of my successes in this endeavor.
UPDATE 19 Oct 2017: We no longer allow character escapes inside our
double-quoted strings for ease of processing. We now internally use
the name "ƔAML" in our code to clarify that this is Not Quite YAML.
Sending love + warmth eternal,
- kibigo [@kibi@glitch.social]
@@ -96,10 +100,7 @@ const ALLOWED_CHAR = unirex( // `c-printable` in the YAML 1.2 spec.
compat_mode ? '[\t\n\r\x20-\x7e\x85\xa0-\ufffd]' : '[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]'
);
const WHITE_SPACE = /[ \t]/;
const INDENTATION = / */; // Indentation must be only spaces.
const LINE_BREAK = /\r?\n|\r|<br\s*\/?>/;
const ESCAPE_CHAR = /[0abt\tnvfre "\/\\N_LP]/;
const HEXADECIMAL_CHARS = /[0-9a-fA-F]/;
const INDICATOR = /[-?:,[\]{}&#*!|>'"%@`]/;
const FLOW_CHAR = /[,[\]{}]/;
@@ -121,7 +122,7 @@ const NEW_LINE = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK)
);
const SOME_NEW_LINES = unirex(
'(?:' + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ')+'
'(?:' + rexstr(NEW_LINE) + ')+'
);
const POSSIBLE_STARTS = unirex(
rexstr(DOCUMENT_START) + rexstr(/<p[^<>]*>/) + '?'
@@ -131,22 +132,13 @@ const POSSIBLE_ENDS = unirex(
rexstr(DOCUMENT_END) + '|' +
rexstr(/<\/p>/)
);
const CHARACTER_ESCAPE = unirex(
rexstr(/\\/) +
'(?:' +
rexstr(ESCAPE_CHAR) + '|' +
rexstr(/x/) + rexstr(HEXADECIMAL_CHARS) + '{2}' + '|' +
rexstr(/u/) + rexstr(HEXADECIMAL_CHARS) + '{4}' + '|' +
rexstr(/U/) + rexstr(HEXADECIMAL_CHARS) + '{8}' +
')'
const QUOTE_CHAR = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')[^"]'
);
const ESCAPED_CHAR = unirex(
rexstr(/(?!["\\])/) + rexstr(NOT_LINE_BREAK) + '|' +
rexstr(CHARACTER_ESCAPE)
);
const ANY_ESCAPED_CHARS = unirex(
rexstr(ESCAPED_CHAR) + '*'
const ANY_QUOTE_CHAR = unirex(
rexstr(QUOTE_CHAR) + '*'
);
const ESCAPED_APOS = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/)
);
@@ -190,120 +182,76 @@ const LATER_VALUE_CHAR = unirex(
/* YAML CONSTRUCTS */
const YAML_START = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(/---/)
const ƔAML_START = unirex(
rexstr(ANY_WHITE_SPACE) + '---'
);
const YAML_END = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(/(?:---|\.\.\.)/)
const ƔAML_END = unirex(
rexstr(ANY_WHITE_SPACE) + '(?:---|\.\.\.)'
);
const YAML_LOOKAHEAD = unirex(
const ƔAML_LOOKAHEAD = unirex(
'(?=' +
rexstr(YAML_START) +
rexstr(ƔAML_START) +
rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) +
rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) +
rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) +
')'
);
const YAML_DOUBLE_QUOTE = unirex(
rexstr(/"/) + rexstr(ANY_ESCAPED_CHARS) + rexstr(/"/)
const ƔAML_DOUBLE_QUOTE = unirex(
'"' + rexstr(ANY_QUOTE_CHAR) + '"'
);
const YAML_SINGLE_QUOTE = unirex(
rexstr(/'/) + rexstr(ANY_ESCAPED_APOS) + rexstr(/'/)
const ƔAML_SINGLE_QUOTE = unirex(
'\'' + rexstr(ANY_ESCAPED_APOS) + '\''
);
const YAML_SIMPLE_KEY = unirex(
const ƔAML_SIMPLE_KEY = unirex(
rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*'
);
const YAML_SIMPLE_VALUE = unirex(
const ƔAML_SIMPLE_VALUE = unirex(
rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*'
);
const YAML_KEY = unirex(
rexstr(YAML_DOUBLE_QUOTE) + '|' +
rexstr(YAML_SINGLE_QUOTE) + '|' +
rexstr(YAML_SIMPLE_KEY)
const ƔAML_KEY = unirex(
rexstr(ƔAML_DOUBLE_QUOTE) + '|' +
rexstr(ƔAML_SINGLE_QUOTE) + '|' +
rexstr(ƔAML_SIMPLE_KEY)
);
const YAML_VALUE = unirex(
rexstr(YAML_DOUBLE_QUOTE) + '|' +
rexstr(YAML_SINGLE_QUOTE) + '|' +
rexstr(YAML_SIMPLE_VALUE)
const ƔAML_VALUE = unirex(
rexstr(ƔAML_DOUBLE_QUOTE) + '|' +
rexstr(ƔAML_SINGLE_QUOTE) + '|' +
rexstr(ƔAML_SIMPLE_VALUE)
);
const YAML_SEPARATOR = unirex(
const ƔAML_SEPARATOR = unirex(
rexstr(ANY_WHITE_SPACE) +
':' + rexstr(WHITE_SPACE) +
rexstr(ANY_WHITE_SPACE)
);
const YAML_LINE = unirex(
'(' + rexstr(YAML_KEY) + ')' +
rexstr(YAML_SEPARATOR) +
'(' + rexstr(YAML_VALUE) + ')'
const ƔAML_LINE = unirex(
'(' + rexstr(ƔAML_KEY) + ')' +
rexstr(ƔAML_SEPARATOR) +
'(' + rexstr(ƔAML_VALUE) + ')'
);
/* FRONTMATTER REGEX */
const YAML_FRONTMATTER = unirex(
const ƔAML_FRONTMATTER = unirex(
rexstr(POSSIBLE_STARTS) +
rexstr(YAML_LOOKAHEAD) +
rexstr(YAML_START) + rexstr(SOME_NEW_LINES) +
rexstr(ƔAML_LOOKAHEAD) +
rexstr(ƔAML_START) + rexstr(SOME_NEW_LINES) +
'(?:' +
'(' + rexstr(INDENTATION) + ')' +
rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) +
'(?:' +
'\\1' + rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) +
'){0,4}' +
')?' +
rexstr(YAML_END) + rexstr(POSSIBLE_ENDS)
rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) + rexstr(SOME_NEW_LINES) +
'){0,5}' +
rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS)
);
/* SEARCHES */
const FIND_YAML_LINES = unirex(
rexstr(NEW_LINE) + rexstr(INDENTATION) + rexstr(YAML_LINE)
const FIND_ƔAML_LINE = unirex(
rexstr(NEW_LINE) + rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE)
);
/* STRING PROCESSING */
function processString(str) {
function processString (str) {
switch (str.charAt(0)) {
case '"':
return str
.substring(1, str.length - 1)
.replace(/\\0/g, '\x00')
.replace(/\\a/g, '\x07')
.replace(/\\b/g, '\x08')
.replace(/\\t/g, '\x09')
.replace(/\\\x09/g, '\x09')
.replace(/\\n/g, '\x0a')
.replace(/\\v/g, '\x0b')
.replace(/\\f/g, '\x0c')
.replace(/\\r/g, '\x0d')
.replace(/\\e/g, '\x1b')
.replace(/\\ /g, '\x20')
.replace(/\\"/g, '\x22')
.replace(/\\\//g, '\x2f')
.replace(/\\\\/g, '\x5c')
.replace(/\\N/g, '\x85')
.replace(/\\_/g, '\xa0')
.replace(/\\L/g, '\u2028')
.replace(/\\P/g, '\u2029')
.replace(
new RegExp(
unirex(
rexstr(/\\x/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{2})'
), 'gu'
), (_, n) => String.fromCodePoint('0x' + n)
)
.replace(
new RegExp(
unirex(
rexstr(/\\u/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{4})'
), 'gu'
), (_, n) => String.fromCodePoint('0x' + n)
)
.replace(
new RegExp(
unirex(
rexstr(/\\U/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{8})'
), 'gu'
), (_, n) => String.fromCodePoint('0x' + n)
);
return str.substring(1, str.length - 1);
case '\'':
return str
.substring(1, str.length - 1)
@@ -321,15 +269,18 @@ export function processBio(content) {
text: content,
metadata: [],
};
let yaml = content.match(YAML_FRONTMATTER);
if (!yaml) return result;
else yaml = yaml[0];
let start = content.search(YAML_START);
let end = start + yaml.length - yaml.search(YAML_START);
result.text = content.substr(0, start) + content.substr(end);
let ɣaml = content.match(ƔAML_FRONTMATTER);
if (!ɣaml) {
return result;
} else {
ɣaml = ɣaml[0];
}
const start = content.search(ƔAML_START);
const end = start + ɣaml.length - ɣaml.search(ƔAML_START);
result.text = content.substr(end);
let metadata = null;
let query = new RegExp(FIND_YAML_LINES, 'g');
while ((metadata = query.exec(yaml))) {
let query = new RegExp(rexstr(FIND_ƔAML_LINE), 'g'); // Some browsers don't allow flags unless both args are strings
while ((metadata = query.exec(ɣaml))) {
result.metadata.push([
processString(metadata[1]),
processString(metadata[2]),
@@ -352,63 +303,23 @@ export function createBio(note, data) {
let val = '' + data[i][1];
// Key processing
if (key === (key.match(YAML_SIMPLE_KEY) || [])[0]) /* do nothing */;
else if (key.indexOf('\'') === -1 && key === (key.match(ANY_ESCAPED_APOS) || [])[0]) key = '\'' + key + '\'';
if (key === (key.match(ƔAML_SIMPLE_KEY) || [])[0]) /* do nothing */;
else if (key === (key.match(ANY_QUOTE_CHAR) || [])[0]) key = '"' + key + '"';
else {
key = key
.replace(/\x00/g, '\\0')
.replace(/\x07/g, '\\a')
.replace(/\x08/g, '\\b')
.replace(/\x0a/g, '\\n')
.replace(/\x0b/g, '\\v')
.replace(/\x0c/g, '\\f')
.replace(/\x0d/g, '\\r')
.replace(/\x1b/g, '\\e')
.replace(/\x22/g, '\\"')
.replace(/\x5c/g, '\\\\');
let badchars = key.match(
new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu')
) || [];
for (let j = 0; j < badchars.length; j++) {
key = key.replace(
badchars[i],
'\\u' + badchars[i].codePointAt(0).toLocaleString('en', {
useGrouping: false,
minimumIntegerDigits: 4,
})
);
}
key = '"' + key + '"';
.replace(/'/g, '\'\'')
.replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<EFBFBD>');
key = '\'' + key + '\'';
}
// Value processing
if (val === (val.match(YAML_SIMPLE_VALUE) || [])[0]) /* do nothing */;
else if (val.indexOf('\'') === -1 && val === (val.match(ANY_ESCAPED_APOS) || [])[0]) val = '\'' + val + '\'';
if (val === (val.match(ƔAML_SIMPLE_VALUE) || [])[0]) /* do nothing */;
else if (val === (val.match(ANY_QUOTE_CHAR) || [])[0]) val = '"' + val + '"';
else {
val = val
.replace(/\x00/g, '\\0')
.replace(/\x07/g, '\\a')
.replace(/\x08/g, '\\b')
.replace(/\x0a/g, '\\n')
.replace(/\x0b/g, '\\v')
.replace(/\x0c/g, '\\f')
.replace(/\x0d/g, '\\r')
.replace(/\x1b/g, '\\e')
.replace(/\x22/g, '\\"')
.replace(/\x5c/g, '\\\\');
let badchars = val.match(
new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu')
) || [];
for (let j = 0; j < badchars.length; j++) {
val = val.replace(
badchars[i],
'\\u' + badchars[i].codePointAt(0).toLocaleString('en', {
useGrouping: false,
minimumIntegerDigits: 4,
})
);
}
val = '"' + val + '"';
key = key
.replace(/'/g, '\'\'')
.replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<EFBFBD>');
key = '\'' + key + '\'';
}
frontmatter += key + ': ' + val + '\n';

View File

@@ -8,6 +8,7 @@ import {
refreshHomeTimeline,
refreshCommunityTimeline,
refreshPublicTimeline,
refreshDirectTimeline,
} from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
@@ -133,6 +134,8 @@ export function submitCompose() {
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertOrRefresh('community', refreshCommunityTimeline);
insertOrRefresh('public', refreshPublicTimeline);
} else if (response.data.visibility === 'direct') {
insertOrRefresh('direct', refreshDirectTimeline);
}
}).catch(function (error) {
dispatch(submitComposeFail(error));

View File

@@ -92,3 +92,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
export const connectPublicStream = () => connectTimelineStream('public', 'public');
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');

View File

@@ -115,6 +115,7 @@ export function refreshTimeline(timelineId, path, params = {}) {
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct');
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
@@ -155,6 +156,7 @@ export function expandTimeline(timelineId, path, params = {}) {
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
export const expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct');
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);

View File

@@ -1,5 +1,5 @@
import 'intl';
import 'intl/locale-data/jsonp/en.js';
import 'intl/locale-data/jsonp/en';
import 'es6-symbol/implement';
import includes from 'array-includes';
import assign from 'object-assign';

View File

@@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
<div
className="account__avatar"
data-avatar-of="@alice"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
style={
Object {
"backgroundImage": "url(/animated/alice.gif)",
"backgroundSize": "100px 100px",
"height": "100px",
"width": "100px",
}
}
/>
`;
exports[`<Avatar /> Still renders a still avatar 1`] = `
<div
className="account__avatar"
data-avatar-of="@alice"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
style={
Object {
"backgroundImage": "url(/static/alice.jpg)",
"backgroundSize": "100px 100px",
"height": "100px",
"width": "100px",
}
}
/>
`;

View File

@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AvatarOverlay renders a overlay avatar 1`] = `
<div
className="account__avatar-overlay"
>
<div
className="account__avatar-overlay-base"
data-avatar-of="@alice"
style={
Object {
"backgroundImage": "url(/static/alice.jpg)",
}
}
/>
<div
className="account__avatar-overlay-overlay"
data-avatar-of="@eve@blackhat.lair"
style={
Object {
"backgroundImage": "url(/static/eve.jpg)",
}
}
/>
</div>
`;

View File

@@ -0,0 +1,130 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
<button
className="button button-secondary"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
exports[`<Button /> renders a button element 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
<button
className="button"
disabled={true}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
exports[`<Button /> renders class="button--block" if props.block given 1`] = `
<button
className="button button--block"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
exports[`<Button /> renders the children 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
>
<p>
children
</p>
</button>
`;
exports[`<Button /> renders the given text 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
>
foo
</button>
`;
exports[`<Button /> renders the props.text instead of children 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
>
foo
</button>
`;
exports[`<Button /> renders title if props.title is given 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
title="foo"
/>
`;

View File

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DisplayName /> renders display name + account name 1`] = `
<span
className="display-name"
>
<strong
className="display-name__html"
dangerouslySetInnerHTML={
Object {
"__html": "<p>Foo</p>",
}
}
/>
<span
className="display-name__account"
>
@
bar@baz
</span>
</span>
`;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { fromJS } from 'immutable';
import Avatar from '../avatar';
describe('<Avatar />', () => {
const account = fromJS({
username: 'alice',
acct: 'alice',
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
});
const size = 100;
describe('Autoplay', () => {
it('renders a animated avatar', () => {
const component = renderer.create(<Avatar account={account} animate size={size} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('Still', () => {
it('renders a still avatar', () => {
const component = renderer.create(<Avatar account={account} size={size} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
// TODO add autoplay test if possible
});

View File

@@ -0,0 +1,29 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { fromJS } from 'immutable';
import AvatarOverlay from '../avatar_overlay';
describe('<AvatarOverlay', () => {
const account = fromJS({
username: 'alice',
acct: 'alice',
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
});
const friend = fromJS({
username: 'eve',
acct: 'eve@blackhat.lair',
display_name: 'Evelyn',
avatar: '/animated/eve.gif',
avatar_static: '/static/eve.jpg',
});
it('renders a overlay avatar', () => {
const component = renderer.create(<AvatarOverlay account={account} friend={friend} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,82 @@
import { shallow } from 'enzyme';
import React from 'react';
import renderer from 'react-test-renderer';
import Button from '../button';
describe('<Button />', () => {
it('renders a button element', () => {
const component = renderer.create(<Button />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders the given text', () => {
const text = 'foo';
const component = renderer.create(<Button text={text} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('handles click events using the given handler', () => {
const handler = jest.fn();
const button = shallow(<Button onClick={handler} />);
button.find('button').simulate('click');
expect(handler.mock.calls.length).toEqual(1);
});
it('does not handle click events if props.disabled given', () => {
const handler = jest.fn();
const button = shallow(<Button onClick={handler} disabled />);
button.find('button').simulate('click');
expect(handler.mock.calls.length).toEqual(0);
});
it('renders a disabled attribute if props.disabled given', () => {
const component = renderer.create(<Button disabled />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders the children', () => {
const children = <p>children</p>;
const component = renderer.create(<Button>{children}</Button>);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders the props.text instead of children', () => {
const text = 'foo';
const children = <p>children</p>;
const component = renderer.create(<Button text={text}>{children}</Button>);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders class="button--block" if props.block given', () => {
const component = renderer.create(<Button block />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('adds class "button-secondary" if props.secondary given', () => {
const component = renderer.create(<Button secondary />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders title if props.title is given', () => {
const component = renderer.create(<Button title='foo' />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,18 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { fromJS } from 'immutable';
import DisplayName from '../display_name';
describe('<DisplayName />', () => {
it('renders display name + account name', () => {
const account = fromJS({
username: 'bar',
acct: 'bar@baz',
display_name_html: '<p>Foo</p>',
});
const component = renderer.create(<DisplayName account={account} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -14,6 +14,7 @@ export default class Button extends React.PureComponent {
className: PropTypes.string,
style: PropTypes.object,
children: PropTypes.node,
title: PropTypes.string,
};
static defaultProps = {
@@ -35,26 +36,26 @@ export default class Button extends React.PureComponent {
}
render () {
const style = {
padding: `0 ${this.props.size / 2.25}px`,
height: `${this.props.size}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
let attrs = {
className: classNames('button', this.props.className, {
'button-secondary': this.props.secondary,
'button--block': this.props.block,
}),
disabled: this.props.disabled,
onClick: this.handleClick,
ref: this.setRef,
style: {
padding: `0 ${this.props.size / 2.25}px`,
height: `${this.props.size}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
},
};
const className = classNames('button', this.props.className, {
'button-secondary': this.props.secondary,
'button--block': this.props.block,
});
if (this.props.title) attrs.title = this.props.title;
return (
<button
className={className}
disabled={this.props.disabled}
onClick={this.handleClick}
ref={this.setRef}
style={style}
>
<button {...attrs}>
{this.props.text || this.props.children}
</button>
);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import Motion from 'react-motion/lib/Motion';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types';

View File

@@ -175,7 +175,9 @@ export default class ColumnHeader extends React.PureComponent {
<div className={wrapperClassName}>
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
{title}
<span className='column-header__title'>
{title}
</span>
<div className='column-header__buttons'>
{backButton}
{ notifCleaning ? (

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from './icon_button';
import Overlay from 'react-overlays/lib/Overlay';
import Motion from 'react-motion/lib/Motion';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import detectPassiveEvents from 'detect-passive-events';

View File

@@ -1,7 +1,8 @@
import React from 'react';
import Motion from 'react-motion/lib/Motion';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class IconButton extends React.PureComponent {
@@ -56,27 +57,26 @@ export default class IconButton extends React.PureComponent {
style.textAlign = 'left';
}
const classes = ['icon-button'];
const {
active,
animate,
className,
disabled,
expanded,
icon,
inverted,
overlay,
pressed,
tabIndex,
title,
} = this.props;
if (this.props.active) {
classes.push('active');
}
if (this.props.disabled) {
classes.push('disabled');
}
if (this.props.inverted) {
classes.push('inverted');
}
if (this.props.overlay) {
classes.push('overlayed');
}
if (this.props.className) {
classes.push(this.props.className);
}
const classes = classNames(className, 'icon-button', {
active,
disabled,
inverted,
overlayed: overlay,
});
const flipDeg = this.props.flip ? -180 : -360;
const rotateDeg = this.props.active ? flipDeg : 0;
@@ -90,23 +90,23 @@ export default class IconButton extends React.PureComponent {
damping: 7,
};
const motionStyle = {
rotate: this.props.animate ? spring(rotateDeg, springOpts) : 0,
rotate: animate ? spring(rotateDeg, springOpts) : 0,
};
return (
<Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
{({ rotate }) =>
<button
aria-label={this.props.title}
aria-pressed={this.props.pressed}
aria-expanded={this.props.expanded}
title={this.props.title}
className={classes.join(' ')}
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
style={style}
tabIndex={this.props.tabIndex}
tabIndex={tabIndex}
>
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
{this.props.label}
</button>
}

View File

@@ -16,6 +16,7 @@ const messages = defineMessages({
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
share: { id: 'status.share', defaultMessage: 'Share' },
more: { id: 'status.more', defaultMessage: 'More' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
@@ -182,7 +183,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
{shareButton}
<div className='status__action-bar-dropdown'>
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
</div>
</div>
);

View File

@@ -125,6 +125,7 @@ export default class StatusContent extends React.PureComponent {
const directionStyle = { direction: 'ltr' };
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
});
if (isRtl(status.get('search_index'))) {
@@ -156,7 +157,7 @@ export default class StatusContent extends React.PureComponent {
{mentionsPlaceholder}
<div tabIndex={!hidden && 0} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
</div>
);
} else if (this.props.onClick) {

View File

@@ -10,6 +10,7 @@ import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
const { localeData, messages } = getLocale();
addLocaleData(localeData);

View File

@@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Motion from 'react-motion/lib/Motion';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';

View File

@@ -5,8 +5,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import UploadButtonContainer from '../containers/upload_button_container';
import DoodleButtonContainer from '../containers/doodle_button_container';
import { defineMessages, injectIntl } from 'react-intl';
import Collapsable from '../../../components/collapsable';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
@@ -20,6 +18,7 @@ import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
import ComposeAttachOptions from '../../../../glitch/components/compose/attach_options/index';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -58,7 +57,6 @@ export default class ComposeForm extends ImmutablePureComponent {
onPickEmoji: PropTypes.func.isRequired,
showSearch: PropTypes.bool,
settings : ImmutablePropTypes.map.isRequired,
filesAttached : PropTypes.bool,
};
static defaultProps = {
@@ -156,17 +154,18 @@ export default class ComposeForm extends ImmutablePureComponent {
}
render () {
const { intl, onPaste, showSearch, filesAttached } = this.props;
const { intl, onPaste, showSearch } = this.props;
const disabled = this.props.is_submitting;
const maybeEye = (this.props.advanced_options && this.props.advanced_options.do_not_federate) ? ' 👁️' : '';
const text = [this.props.spoiler_text, countableText(this.props.text), maybeEye].join('');
const secondaryVisibility = this.props.settings.get('side_arm');
const isWideView = this.props.settings.get('stretch');
let showSideArm = secondaryVisibility !== 'none';
let publishText = '';
let publishText2 = '';
let title = '';
let title2 = '';
const privacyIcons = {
none: '',
@@ -176,30 +175,30 @@ export default class ComposeForm extends ImmutablePureComponent {
direct: 'envelope',
};
title = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${this.props.privacy}.short` })}`;
if (showSideArm) {
// Enhanced behavior with dual toot buttons
publishText = (
<span>
{
<i
className={`fa fa-${privacyIcons[this.props.privacy]}`}
style={{
paddingRight: (filesAttached || !isWideView) ? '0' : '5px',
}}
style={{ paddingRight: '5px' }}
/>
}{
(filesAttached || !isWideView) ? '' :
intl.formatMessage(messages.publish)
}
}{intl.formatMessage(messages.publish)}
</span>
);
title2 = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${secondaryVisibility}.short` })}`;
publishText2 = (
<i
className={`fa fa-${privacyIcons[secondaryVisibility]}`}
aria-label={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${secondaryVisibility}.short` })}`}
aria-label={title2}
/>
);
} else {
// Original vanilla behavior - no icon if public or unlisted
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
} else {
@@ -248,12 +247,12 @@ export default class ComposeForm extends ImmutablePureComponent {
</div>
<div className='compose-form__buttons'>
<UploadButtonContainer />
<DoodleButtonContainer />
<PrivacyDropdownContainer />
<ComposeAdvancedOptionsContainer />
<ComposeAttachOptions />
<SensitiveButtonContainer />
<div className='compose-form__buttons-separator' />
<PrivacyDropdownContainer />
<SpoilerButtonContainer />
<ComposeAdvancedOptionsContainer />
</div>
<div className='compose-form__publish'>
@@ -264,6 +263,7 @@ export default class ComposeForm extends ImmutablePureComponent {
<Button
className='compose-form__publish__side-arm'
text={publishText2}
title={title2}
onClick={this.handleSubmit2}
disabled={submitDisabled}
/> : ''
@@ -271,9 +271,9 @@ export default class ComposeForm extends ImmutablePureComponent {
<Button
className='compose-form__publish__primary'
text={publishText}
title={title}
onClick={this.handleSubmit}
disabled={submitDisabled}
block
/>
</div>
</div>

View File

@@ -1,41 +0,0 @@
import React from 'react';
import IconButton from '../../../components/icon_button';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
doodle: { id: 'doodle_button.label', defaultMessage: 'Add a drawing' },
});
const iconStyle = {
height: null,
lineHeight: '27px',
};
@injectIntl
export default class UploadButton extends ImmutablePureComponent {
static propTypes = {
disabled: PropTypes.bool,
onOpenCanvas: PropTypes.func.isRequired,
style: PropTypes.object,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onOpenCanvas();
}
render () {
const { intl, disabled } = this.props;
return (
<div className='compose-form__upload-button'>
<IconButton icon='pencil' title={intl.formatMessage(messages.doodle)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
</div>
);
}
}

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Overlay from 'react-overlays/lib/Overlay';
import Motion from 'react-motion/lib/Motion';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import detectPassiveEvents from 'detect-passive-events';
import classNames from 'classnames';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Overlay from 'react-overlays/lib/Overlay';
import Motion from 'react-motion/lib/Motion';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
const messages = defineMessages({

View File

@@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from '../../../components/icon_button';
import Motion from 'react-motion/lib/Motion';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
@@ -68,7 +68,7 @@ export default class Upload extends ImmutablePureComponent {
<div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => (
<div className='compose-form__upload-thumbnail' style={{ transform: `translateZ(0) scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}>
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}>
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} />
<div className={classNames('compose-form__upload-description', { active })}>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Motion from 'react-motion/lib/Motion';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { FormattedMessage } from 'react-intl';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Motion from 'react-motion/lib/Motion';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
export default class Warning extends React.PureComponent {

View File

@@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import AutosuggestStatus from '../components/autosuggest_status';
import { makeGetStatus } from '../../../selectors';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, { id }) => ({
status: getStatus(state, id),
});
return mapStateToProps;
};
export default connect(makeMapStateToProps)(AutosuggestStatus);

View File

@@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import DoodleButton from '../components/doodle_button';
import { openModal } from '../../../actions/modal';
const mapStateToProps = state => ({
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
});
const mapDispatchToProps = dispatch => ({
onOpenCanvas () {
dispatch(openModal('DOODLE', { noEsc: true }));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DoodleButton);

View File

@@ -46,7 +46,7 @@ const getFrequentlyUsedEmojis = createSelector([
const getCustomEmojis = createSelector([
state => state.get('custom_emojis'),
], emojis => emojis.sort((a, b) => {
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
const aShort = a.get('shortcode').toLowerCase();
const bShort = b.get('shortcode').toLowerCase();

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import IconButton from '../../../components/icon_button';
import { changeComposeSensitivity } from '../../../actions/compose';
import Motion from 'react-motion/lib/Motion';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { injectIntl, defineMessages } from 'react-intl';
@@ -47,7 +47,7 @@ class SensitiveButton extends React.PureComponent {
'compose-form__sensitive-button--visible': visible,
});
return (
<div className={className} style={{ transform: `translateZ(0) scale(${scale})` }}>
<div className={className} style={{ transform: `scale(${scale})` }}>
<IconButton
className='compose-form__sensitive-button__icon'
title={intl.formatMessage(messages.title)}

View File

@@ -10,7 +10,7 @@ import { changeLocalSetting } from '../../../glitch/actions/local_settings';
import { Link } from 'react-router-dom';
import { injectIntl, defineMessages } from 'react-intl';
import SearchContainer from './containers/search_container';
import Motion from 'react-motion/lib/Motion';
import Motion from '../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import SearchResultsContainer from './containers/search_results_container';
import { changeComposing } from '../../actions/compose';
@@ -105,7 +105,7 @@ export default class Compose extends React.PureComponent {
<SearchContainer />
<div className='drawer__pager'>
<div className='drawer__inner' onFocus={this.onFocus}>
<div className='drawer__inner scrollable optionally-scrollable' onFocus={this.onFocus}>
<NavigationContainer onClose={this.onBlur} />
<ComposeFormContainer />
</div>

View File

@@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import ColumnSettings from '../../community_timeline/components/column_settings';
import { changeSetting } from '../../../actions/settings';
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'direct']),
});
const mapDispatchToProps = dispatch => ({
onChange (key, checked) {
dispatch(changeSetting(['direct', ...key], checked));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import {
refreshDirectTimeline,
expandDirectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import { connectDirectStream } from '../../actions/streaming';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
});
@connect(mapStateToProps)
@injectIntl
export default class DirectTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('DIRECT', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
componentDidMount () {
const { dispatch } = this.props;
dispatch(refreshDirectTimeline());
this.disconnect = dispatch(connectDirectStream());
}
componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandDirectTimeline());
}
render () {
const { intl, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='envelope'
active={hasUnread}
title={intl.formatMessage(messages.title)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`direct_timeline-${columnId}`}
timelineId='direct'
loadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
/>
</Column>
);
}
}

View File

@@ -0,0 +1,61 @@
import emojify from '../emoji';
describe('emoji', () => {
describe('.emojify', () => {
it('ignores unknown shortcodes', () => {
expect(emojify(':foobarbazfake:')).toEqual(':foobarbazfake:');
});
it('ignores shortcodes inside of tags', () => {
expect(emojify('<p data-foo=":smile:"></p>')).toEqual('<p data-foo=":smile:"></p>');
});
it('works with unclosed tags', () => {
expect(emojify('hello>')).toEqual('hello>');
expect(emojify('<hello')).toEqual('<hello');
});
it('works with unclosed shortcodes', () => {
expect(emojify('smile:')).toEqual('smile:');
expect(emojify(':smile')).toEqual(':smile');
});
it('does unicode', () => {
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
'<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
expect(emojify('👨‍👩‍👧‍👧')).toEqual(
'<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
expect(emojify('\u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
});
it('does multiple unicode', () => {
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
});
it('ignores unicode inside of tags', () => {
expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).toEqual('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
});
it('does multiple emoji properly (issue 5188)', () => {
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
});
it('does an emoji that has no shortcode', () => {
expect(emojify('🕉️')).toEqual('<img draggable="false" class="emojione" alt="🕉️" title="" src="/emoji/1f549.svg" />');
});
it('does an emoji whose filename is irregular', () => {
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
});
});
});

View File

@@ -0,0 +1,130 @@
import { pick } from 'lodash';
import { emojiIndex } from 'emoji-mart';
import { search } from '../emoji_mart_search_light';
const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']);
describe('emoji_index', () => {
it('should give same result for emoji_index_light and emoji-mart', () => {
const expected = [
{
id: 'pineapple',
unified: '1f34d',
native: '🍍',
},
];
expect(search('pineapple').map(trimEmojis)).toEqual(expected);
expect(emojiIndex.search('pineapple').map(trimEmojis)).toEqual(expected);
});
it('orders search results correctly', () => {
const expected = [
{
id: 'apple',
unified: '1f34e',
native: '🍎',
},
{
id: 'pineapple',
unified: '1f34d',
native: '🍍',
},
{
id: 'green_apple',
unified: '1f34f',
native: '🍏',
},
{
id: 'iphone',
unified: '1f4f1',
native: '📱',
},
];
expect(search('apple').map(trimEmojis)).toEqual(expected);
expect(emojiIndex.search('apple').map(trimEmojis)).toEqual(expected);
});
it('handles custom emoji', () => {
const custom = [
{
id: 'mastodon',
name: 'mastodon',
short_names: ['mastodon'],
text: '',
emoticons: [],
keywords: ['mastodon'],
imageUrl: 'http://example.com',
custom: true,
},
];
search('', { custom });
emojiIndex.search('', { custom });
const expected = [
{
id: 'mastodon',
custom: true,
},
];
expect(search('masto').map(trimEmojis)).toEqual(expected);
expect(emojiIndex.search('masto').map(trimEmojis)).toEqual(expected);
});
it('should filter only emojis we care about, exclude pineapple', () => {
const emojisToShowFilter = unified => unified !== '1F34D';
expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id))
.not.toContain('pineapple');
expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id))
.not.toContain('pineapple');
});
it('can include/exclude categories', () => {
expect(search('flag', { include: ['people'] })).toEqual([]);
expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]);
});
it('does an emoji whose unified name is irregular', () => {
const expected = [
{
'id': 'water_polo',
'unified': '1f93d',
'native': '🤽',
},
{
'id': 'man-playing-water-polo',
'unified': '1f93d-200d-2642-fe0f',
'native': '🤽‍♂️',
},
{
'id': 'woman-playing-water-polo',
'unified': '1f93d-200d-2640-fe0f',
'native': '🤽‍♀️',
},
];
expect(search('polo').map(trimEmojis)).toEqual(expected);
expect(emojiIndex.search('polo').map(trimEmojis)).toEqual(expected);
});
it('can search for thinking_face', () => {
const expected = [
{
id: 'thinking_face',
unified: '1f914',
native: '🤔',
},
];
expect(search('thinking_fac').map(trimEmojis)).toEqual(expected);
expect(emojiIndex.search('thinking_fac').map(trimEmojis)).toEqual(expected);
});
it('can search for woman-facepalming', () => {
const expected = [
{
id: 'woman-facepalming',
unified: '1f926-200d-2640-fe0f',
native: '🤦‍♀️',
},
];
expect(search('woman-facep').map(trimEmojis)).toEqual(expected);
expect(emojiIndex.search('woman-facep').map(trimEmojis)).toEqual(expected);
});
});

View File

@@ -9,7 +9,8 @@ const { unicodeToFilename } = require('./unicode_to_filename');
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
const emojiMap = require('./emoji_map.json');
const { emojiIndex } = require('emoji-mart');
const emojiMartData = require('emoji-mart/dist/data').default;
const { default: emojiMartData } = require('emoji-mart/dist/data');
const excluded = ['®', '©', '™'];
const skins = ['🏻', '🏼', '🏽', '🏾', '🏿'];
const shortcodeMap = {};

View File

@@ -17,6 +17,7 @@ const messages = defineMessages({
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
@@ -78,18 +79,22 @@ export default class GettingStarted extends ImmutablePureComponent {
}
}
navItems = navItems.concat([
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
]);
if (me.get('locked')) {
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />);
}
navItems = navItems.concat([
<ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
<ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
]);
if (me.get('locked')) {
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
}
navItems = navItems.concat([
<ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
]);
return (

View File

@@ -48,6 +48,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
let media = '';
let mediaIcon = null;
let applicationLink = '';
let reblogLink = '';
let reblogIcon = 'retweet';
if (status.get('media_attachments').size > 0) {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
@@ -85,6 +87,23 @@ export default class DetailedStatus extends ImmutablePureComponent {
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
}
if (status.get('visibility') === 'direct') {
reblogIcon = 'envelope';
} else if (status.get('visibility') === 'private') {
reblogIcon = 'lock';
}
if (status.get('visibility') === 'private') {
reblogLink = <i className={`fa fa-${reblogIcon}`} />;
} else {
reblogLink = (<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
<i className={`fa fa-${reblogIcon}`} />
<span className='detailed-status__reblogs'>
<FormattedNumber value={status.get('reblogs_count')} />
</span>
</Link>);
}
return (
<div className='detailed-status'>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
@@ -101,12 +120,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
<i className='fa fa-retweet' />
<span className='detailed-status__reblogs'>
<FormattedNumber value={status.get('reblogs_count')} />
</span>
</Link> · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
</a>{applicationLink} · {reblogLink} · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
<i className='fa fa-star' />
<span className='detailed-status__favorites'>
<FormattedNumber value={status.get('favourites_count')} />

View File

@@ -1,14 +1,18 @@
import { expect } from 'chai';
import { mount } from 'enzyme';
import sinon from 'sinon';
import React from 'react';
import Column from '../../../../../../app/javascript/mastodon/features/ui/components/column';
import ColumnHeader from '../../../../../../app/javascript/mastodon/features/ui/components/column_header';
import { mount } from 'enzyme';
import Column from '../column';
import ColumnHeader from '../column_header';
describe('<Column />', () => {
describe('<ColumnHeader /> click handler', () => {
const originalRaf = global.requestAnimationFrame;
beforeEach(() => {
global.requestAnimationFrame = sinon.spy();
global.requestAnimationFrame = jest.fn();
});
afterAll(() => {
global.requestAnimationFrame = originalRaf;
});
it('runs the scroll animation if the column contains scrollable content', () => {
@@ -18,13 +22,13 @@ describe('<Column />', () => {
</Column>
);
wrapper.find(ColumnHeader).simulate('click');
expect(global.requestAnimationFrame.called).to.equal(true);
expect(global.requestAnimationFrame.mock.calls.length).toEqual(1);
});
it('does not try to scroll if there is no scrollable content', () => {
const wrapper = mount(<Column heading='notifications' />);
wrapper.find(ColumnHeader).simulate('click');
expect(global.requestAnimationFrame.called).to.equal(false);
expect(global.requestAnimationFrame.mock.calls.length).toEqual(0);
});
});
});

View File

@@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from '../../ui/util/async-components';
import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
@@ -23,6 +23,7 @@ const componentMap = {
'PUBLIC': PublicTimeline,
'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline,
'FAVOURITES': FavouritedStatuses,
};

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Motion from 'react-motion/lib/Motion';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { FormattedMessage } from 'react-intl';
@@ -40,7 +40,7 @@ export default class UploadArea extends React.PureComponent {
{({ backgroundOpacity, backgroundScale }) =>
<div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
<div className='upload-area__drop'>
<div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} />
<div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} />
<div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
</div>
</div>

View File

@@ -29,6 +29,7 @@ import {
Following,
Reblogs,
Favourites,
DirectTimeline,
HashtagTimeline,
Notifications,
FollowRequests,
@@ -71,6 +72,7 @@ const keyMap = {
goToNotifications: 'g n',
goToLocal: 'g l',
goToFederated: 'g t',
goToDirect: 'g d',
goToStart: 'g s',
goToFavourites: 'g f',
goToPinned: 'g p',
@@ -302,6 +304,10 @@ export default class UI extends React.Component {
this.context.router.history.push('/timelines/public');
}
handleHotkeyGoToDirect = () => {
this.context.router.history.push('/timelines/direct');
}
handleHotkeyGoToStart = () => {
this.context.router.history.push('/getting-started');
}
@@ -357,6 +363,7 @@ export default class UI extends React.Component {
goToNotifications: this.handleHotkeyGoToNotifications,
goToLocal: this.handleHotkeyGoToLocal,
goToFederated: this.handleHotkeyGoToFederated,
goToDirect: this.handleHotkeyGoToDirect,
goToStart: this.handleHotkeyGoToStart,
goToFavourites: this.handleHotkeyGoToFavourites,
goToPinned: this.handleHotkeyGoToPinned,
@@ -377,6 +384,7 @@ export default class UI extends React.Component {
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
<WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} />

View File

@@ -26,6 +26,10 @@ export function HashtagTimeline () {
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
}
export function DirectTimeline() {
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
}
export function Status () {
return import(/* webpackChunkName: "features/status" */'../../status');
}

View File

@@ -0,0 +1,56 @@
// Like react-motion's Motion, but checks to see if the user prefers
// reduced motion and uses a cross-fade in those cases.
import React from 'react';
import Motion from 'react-motion/lib/Motion';
import PropTypes from 'prop-types';
const stylesToKeep = ['opacity', 'backgroundOpacity'];
let reduceMotion;
const extractValue = (value) => {
// This is either an object with a "val" property or it's a number
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
};
class OptionalMotion extends React.Component {
static propTypes = {
defaultStyle: PropTypes.object,
style: PropTypes.object,
children: PropTypes.func,
}
render() {
const { style, defaultStyle, children } = this.props;
if (typeof reduceMotion !== 'boolean') {
// This never changes without a page reload, so we can just grab it
// once from the body classes as opposed to using Redux's connect(),
// which would unnecessarily update every state change
reduceMotion = document.body.classList.contains('reduce-motion');
}
if (reduceMotion) {
Object.keys(style).forEach(key => {
if (stylesToKeep.includes(key)) {
return;
}
// If it's setting an x or height or scale or some other value, we need
// to preserve the end-state value without actually animating it
style[key] = defaultStyle[key] = extractValue(style[key]);
});
}
return (
<Motion style={style} defaultStyle={defaultStyle}>
{children}
</Motion>
);
}
}
export default OptionalMotion;

View File

@@ -82,7 +82,6 @@
"empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.",
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
"empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
"empty_column.home.inactivity": "صفحتك فارغة. لقد كنت غائبا لفترة طويلة عن الشبكة. سوف تملأ تلقائيا في وقت قريب.",
"empty_column.home.public_timeline": "الخيط العام",
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
"empty_column.public": "لا يوجد شيء هنا ! قم بتحرير شيء ما بشكل عام، أو اتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام.",
@@ -160,6 +159,11 @@
"privacy.public.short": "للعامة",
"privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة",
"privacy.unlisted.short": "غير مدرج",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "إلغاء",
"report.placeholder": "تعليقات إضافية",
"report.submit": "إرسال",
@@ -179,6 +183,7 @@
"status.load_more": "حمّل المزيد",
"status.media_hidden": "الصورة مستترة",
"status.mention": "أذكُر @{name}",
"status.more": "More",
"status.mute_conversation": "كتم المحادثة",
"status.open": "وسع هذه المشاركة",
"status.pin": "تدبيس على الملف الشخصي",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -160,6 +159,11 @@
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not show in public timelines",
"privacy.unlisted.short": "Unlisted",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Отказ",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
@@ -179,6 +183,7 @@
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Споменаване",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per fer rodar la pilota!",
"empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
"empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
"empty_column.home.inactivity": "La línia Inici és buida. si has estat inactiu durant un temps es regenerarà aviat.",
"empty_column.home.public_timeline": "la línia de temps pública",
"empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
"empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho",
@@ -160,11 +159,11 @@
"privacy.public.short": "Públic",
"privacy.unlisted.long": "No publicar en línies de temps públiques",
"privacy.unlisted.short": "No llistat",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.days": "fa {number} jorns",
"relative_time.hours": "fa {number} hores",
"relative_time.just_now": "ara",
"relative_time.minutes": "fa {number} minutes",
"relative_time.seconds": "fa {number} segondes",
"reply_indicator.cancel": "Cancel·lar",
"report.placeholder": "Comentaris addicionals",
"report.submit": "Enviar",
@@ -184,6 +183,7 @@
"status.load_more": "Carrega més",
"status.media_hidden": "Multimèdia amagat",
"status.mention": "Esmentar @{name}",
"status.more": "Més",
"status.mute_conversation": "Silenciar conversació",
"status.open": "Ampliar aquest estat",
"status.pin": "Fixat en el perfil",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe einen öffentlichen Beitrag, um den Ball ins Rollen zu bringen!",
"empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
"empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
"empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv warst, wird sie für dich so schnell wie möglich neu erstellt.",
"empty_column.home.public_timeline": "die öffentliche Zeitleiste",
"empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.",
"empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um die Zeitleiste aufzufüllen",
@@ -160,6 +159,11 @@
"privacy.public.short": "Öffentlich",
"privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen",
"privacy.unlisted.short": "Nicht gelistet",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Abbrechen",
"report.placeholder": "Zusätzliche Kommentare",
"report.submit": "Absenden",
@@ -179,6 +183,7 @@
"status.load_more": "Weitere laden",
"status.media_hidden": "Medien versteckt",
"status.mention": "@{name} erwähnen",
"status.more": "Mehr",
"status.mute_conversation": "Thread stummschalten",
"status.open": "Diesen Beitrag öffnen",
"status.pin": "Im Profil anheften",

View File

@@ -183,6 +183,10 @@
"defaultMessage": "Share",
"id": "status.share"
},
{
"defaultMessage": "More",
"id": "status.more"
},
{
"defaultMessage": "Reply to thread",
"id": "status.replyAll"
@@ -751,6 +755,19 @@
],
"path": "app/javascript/mastodon/features/compose/index.json"
},
{
"descriptors": [
{
"defaultMessage": "Direct messages",
"id": "column.direct"
},
{
"defaultMessage": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
"id": "empty_column.direct"
}
],
"path": "app/javascript/mastodon/features/direct_timeline/index.json"
},
{
"descriptors": [
{
@@ -812,6 +829,10 @@
"defaultMessage": "Local timeline",
"id": "navigation_bar.community_timeline"
},
{
"defaultMessage": "Direct messages",
"id": "navigation_bar.direct"
},
{
"defaultMessage": "Preferences",
"id": "navigation_bar.preferences"
@@ -907,10 +928,6 @@
"defaultMessage": "Home",
"id": "column.home"
},
{
"defaultMessage": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"id": "empty_column.home.inactivity"
},
{
"defaultMessage": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"id": "empty_column.home"
@@ -1408,4 +1425,4 @@
],
"path": "app/javascript/mastodon/features/video/index.json"
}
]
]

View File

@@ -28,6 +28,7 @@
"bundle_modal_error.retry": "Try again",
"column.blocks": "Blocked users",
"column.community": "Local timeline",
"column.direct": "Direct messages",
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Home",
@@ -80,9 +81,9 @@
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -107,6 +108,7 @@
"missing_indicator.label": "Not found",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.direct": "Direct messages",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
@@ -160,6 +162,11 @@
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not post to public timelines",
"privacy.unlisted.short": "Unlisted",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Cancel",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
@@ -179,6 +186,7 @@
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",

View File

@@ -1,216 +1,221 @@
{
"account.block": "Bloki @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.block_domain": "Kaŝi ĉion el {domain}",
"account.disclaimer_full": "La ĉi-subaj informoj povas ne plene reflekti la profilon de la uzanto.",
"account.edit_profile": "Redakti la profilon",
"account.follow": "Sekvi",
"account.followers": "Sekvantoj",
"account.follows": "Sekvatoj",
"account.follows_you": "Sekvas vin",
"account.media": "Media",
"account.media": "Sonbildaĵoj",
"account.mention": "Mencii @{name}",
"account.mute": "Mute @{name}",
"account.mute": "Silentigi @{name}",
"account.posts": "Mesaĝoj",
"account.report": "Report @{name}",
"account.report": "Signali @{name}",
"account.requested": "Atendas aprobon",
"account.share": "Share @{name}'s profile",
"account.share": "Diskonigi la profilon de @{name}",
"account.unblock": "Malbloki @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Malsekvi",
"account.unmute": "Unmute @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"column.blocks": "Blocked users",
"account.unblock_domain": "Malkaŝi {domain}",
"account.unfollow": "Ne plus sekvi",
"account.unmute": "Malsilentigi @{name}",
"account.view_full_profile": "Vidi plenan profilon",
"boost_modal.combo": "La proksiman fojon, premu {combo} por pasigi",
"bundle_column_error.body": "Io malfunkciis ŝargante tiun ĉi komponanton.",
"bundle_column_error.retry": "Bonvolu reprovi",
"bundle_column_error.title": "Reta eraro",
"bundle_modal_error.close": "Fermi",
"bundle_modal_error.message": "Io malfunkciis ŝargante tiun ĉi komponanton.",
"bundle_modal_error.retry": "Bonvolu reprovi",
"column.blocks": "Blokitaj uzantoj",
"column.community": "Loka tempolinio",
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.favourites": "Favoritoj",
"column.follow_requests": "Abonpetoj",
"column.home": "Hejmo",
"column.mutes": "Muted users",
"column.mutes": "Silentigitaj uzantoj",
"column.notifications": "Sciigoj",
"column.pins": "Pinned toot",
"column.pins": "Alpinglitaj pepoj",
"column.public": "Fratara tempolinio",
"column_back_button.label": "Reveni",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked",
"column_header.hide_settings": "Kaŝi agordojn",
"column_header.moveLeft_settings": "Movi kolumnon maldekstren",
"column_header.moveRight_settings": "Movi kolumnon dekstren",
"column_header.pin": "Alpingli",
"column_header.show_settings": "Malkaŝi agordojn",
"column_header.unpin": "Depingli",
"column_subheading.navigation": "Navigado",
"column_subheading.settings": "Agordoj",
"compose_form.lock_disclaimer": "Via konta ne estas ŝlosita. Iu ajn povas sekvi vin por vidi viajn privatajn pepojn.",
"compose_form.lock_disclaimer.lock": "ŝlosita",
"compose_form.placeholder": "Pri kio vi pensas?",
"compose_form.publish": "Hup",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
"compose_form.spoiler_placeholder": "Content warning",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ",
"compose_form.spoiler_placeholder": "Skribu tie vian averton",
"confirmation_modal.cancel": "Malfari",
"confirmations.block.confirm": "Bloki",
"confirmations.block.message": "Ĉu vi konfirmas la blokadon de {name}?",
"confirmations.delete.confirm": "Malaperigi",
"confirmations.delete.message": "Ĉu vi konfirmas la malaperigon de tiun pepon?",
"confirmations.domain_block.confirm": "Kaŝi la tutan reton",
"confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas bloki {domain} tute? Plej ofte, kelkaj celitaj blokadoj aŭ silentigoj estas sufiĉaj kaj preferindaj.",
"confirmations.mute.confirm": "Silentigi",
"confirmations.mute.message": "Ĉu vi konfirmas la silentigon de {name}?",
"confirmations.unfollow.confirm": "Ne plu sekvi",
"confirmations.unfollow.message": "Ĉu vi volas ĉesi sekvi {name}?",
"embed.instructions": "Enmetu tiun statkonigon ĉe vian retejon kopiante la ĉi-suban kodon.",
"embed.preview": "Ĝi aperos tiel:",
"emoji_button.activity": "Aktivecoj",
"emoji_button.custom": "Personaj",
"emoji_button.flags": "Flagoj",
"emoji_button.food": "Manĝi kaj trinki",
"emoji_button.label": "Enmeti mieneton",
"emoji_button.nature": "Naturo",
"emoji_button.not_found": "Neniuj mienetoj!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objektoj",
"emoji_button.people": "Homoj",
"emoji_button.recent": "Ofte uzataj",
"emoji_button.search": "Serĉo…",
"emoji_button.search_results": "Rezultatoj de serĉo",
"emoji_button.symbols": "Simboloj",
"emoji_button.travel": "Vojaĝoj & lokoj",
"empty_column.community": "La loka tempolinio estas malplena. Skribu ion por plenigi ĝin!",
"empty_column.hashtag": "Ĝise, neniu enhavo estas asociita kun tiu kradvorto.",
"empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.",
"empty_column.home.public_timeline": "la publika tempolinio",
"empty_column.notifications": "Vi dume ne havas sciigojn. Interagi kun aliajn uzantojn por komenci la konversacion.",
"empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj instancoj por plenigi la publikan tempolinion.",
"follow_request.authorize": "Akcepti",
"follow_request.reject": "Rifuzi",
"getting_started.appsshort": "Aplikaĵoj",
"getting_started.faq": "Oftaj demandoj",
"getting_started.heading": "Por komenci",
"getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}.",
"getting_started.userguide": "User Guide",
"home.column_settings.advanced": "Advanced",
"home.column_settings.basic": "Basic",
"home.column_settings.filter_regex": "Filter out by regular expressions",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"getting_started.open_source_notice": "Mastodono estas malfermkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {github}.",
"getting_started.userguide": "Gvidilo de uzo",
"home.column_settings.advanced": "Precizaj agordoj",
"home.column_settings.basic": "Bazaj agordoj",
"home.column_settings.filter_regex": "Forfiltri per regulesprimo",
"home.column_settings.show_reblogs": "Montri diskonigojn",
"home.column_settings.show_replies": "Montri respondojn",
"home.settings": "Agordoj de la kolumno",
"lightbox.close": "Fermi",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Ŝarĝanta...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
"navigation_bar.blocks": "Blocked users",
"lightbox.next": "Malantaŭa",
"lightbox.previous": "Antaŭa",
"loading_indicator.label": "Ŝarganta",
"media_gallery.toggle_visible": "Baskuli videblecon",
"missing_indicator.label": "Ne trovita",
"navigation_bar.blocks": "Blokitaj uzantoj",
"navigation_bar.community_timeline": "Loka tempolinio",
"navigation_bar.edit_profile": "Redakti la profilon",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "Extended information",
"navigation_bar.favourites": "Favoritaj",
"navigation_bar.follow_requests": "Abonpetoj",
"navigation_bar.info": "Plia informo",
"navigation_bar.logout": "Elsaluti",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.mutes": "Silentigitaj uzantoj",
"navigation_bar.pins": "Alpinglitaj pepoj",
"navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara tempolinio",
"notification.favourite": "{name} favoris vian mesaĝon",
"notification.follow": "{name} sekvis vin",
"notification.mention": "{name} menciis vin",
"notification.reblog": "{name} diskonigis vian mesaĝon",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.clear": "Forviŝi la sciigojn",
"notifications.clear_confirmation": "Ĉu vi certe volas malaperigi ĉiujn viajn sciigojn?",
"notifications.column_settings.alert": "Retumilaj atentigoj",
"notifications.column_settings.favourite": "Favoroj:",
"notifications.column_settings.favourite": "Favoritoj:",
"notifications.column_settings.follow": "Novaj sekvantoj:",
"notifications.column_settings.mention": "Mencioj:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.push": "Puŝsciigoj",
"notifications.column_settings.push_meta": "Tiu ĉi aparato",
"notifications.column_settings.reblog": "Diskonigoj:",
"notifications.column_settings.show": "Montri en kolono",
"notifications.column_settings.sound": "Play sound",
"onboarding.done": "Done",
"onboarding.next": "Next",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
"onboarding.page_one.welcome": "Welcome to Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobile apps",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
"onboarding.skip": "Skip",
"privacy.change": "Adjust status privacy",
"privacy.direct.long": "Post to mentioned users only",
"privacy.direct.short": "Direct",
"privacy.private.long": "Post to followers only",
"privacy.private.short": "Followers-only",
"privacy.public.long": "Post to public timelines",
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not show in public timelines",
"privacy.unlisted.short": "Unlisted",
"reply_indicator.cancel": "Rezigni",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
"report.target": "Reporting",
"notifications.column_settings.sound": "Eligi sonon",
"onboarding.done": "Farita",
"onboarding.next": "Malantaŭa",
"onboarding.page_five.public_timelines": "La loka tempolinio enhavas mesaĝojn de ĉiuj ĉe {domain}. La federacia tempolinio enhavas ĉiujn mesaĝojn de uzantoj, kiujn iu ĉe {domain} sekvas. Ambaŭ tre utilas por trovi novajn kunparolantojn.",
"onboarding.page_four.home": "La hejma tempolinio enhavas la mesaĝojn de ĉiuj uzantoj, kiuj vi sekvas.",
"onboarding.page_four.notifications": "La sciiga kolumno informas vin kiam iu interagas kun vi.",
"onboarding.page_one.federation": "Mastodono estas reto de nedependaj serviloj, unuiĝintaj por krei pligrandan socian retejon. Ni nomas tiujn servilojn instancoj.",
"onboarding.page_one.handle": "Vi estas ĉe {domain}, unu el la multaj instancoj de Mastodono. Via kompleta uznomo do estas {handle}",
"onboarding.page_one.welcome": "Bonvenon al Mastodono!",
"onboarding.page_six.admin": "Via instancestro estas {admin}.",
"onboarding.page_six.almost_done": "Estas preskaŭ finita…",
"onboarding.page_six.appetoot": "Bonan apepiton!",
"onboarding.page_six.apps_available": "{apps} estas elŝuteblaj por iOS, Androido kaj alioj. Kaj nun… bonan apepiton!",
"onboarding.page_six.github": "Mastodono estas libera, senpaga kaj malfermkoda programaro. Vi povas signali cimojn, proponi funkciojn aŭ kontribui al gîa kreskado ĉe {github}.",
"onboarding.page_six.guidelines": "komunreguloj",
"onboarding.page_six.read_guidelines": "Ni petas vin: ne forgesu legi la {guidelines}n de {domain}!",
"onboarding.page_six.various_app": "telefon-aplikaĵoj",
"onboarding.page_three.profile": "Redaktu vian profilon por ŝanĝi vian avataron, priskribon kaj vian nomon. Vi tie trovos ankoraŭ aliajn agordojn.",
"onboarding.page_three.search": "Uzu la serĉokampo por trovi uzantojn kaj esplori kradvortojn tiel ke {illustration} kaj {introductions}. Por trovi iun, kiu ne estas ĉe ĉi tiu instanco, uzu ĝian kompletan uznomon.",
"onboarding.page_two.compose": "Skribu pepojn en la verkkolumno. Vi povas aldoni bildojn, ŝanĝi la agordojn de privateco kaj aldoni tiklavertojn («content warning») dank' al la piktogramoj malsupre.",
"onboarding.skip": "Pasigi",
"privacy.change": "Austigi la privateco de la mesaĝo",
"privacy.direct.long": "Vidigi nur al la menciitaj personoj",
"privacy.direct.short": "Rekta",
"privacy.private.long": "Vidigi nur al viaj sekvantoj",
"privacy.private.short": "Nursekvanta",
"privacy.public.long": "Vidigi en publikaj tempolinioj",
"privacy.public.short": "Publika",
"privacy.unlisted.long": "Ne vidigi en publikaj tempolinioj",
"privacy.unlisted.short": "Nelistigita",
"relative_time.days": "{number}t",
"relative_time.hours": "{number}h",
"relative_time.just_now": "nun",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Malfari",
"report.placeholder": "Pliaj komentoj",
"report.submit": "Sendi",
"report.target": "Signalaĵo",
"search.placeholder": "Serĉi",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"search_popout.search_format": "Detala serĉo",
"search_popout.tips.hashtag": "kradvorto",
"search_popout.tips.status": "statkonigo",
"search_popout.tips.text": "Simpla teksto eligas la kongruajn afiŝnomojn, uznomojn kaj kradvortojn.",
"search_popout.tips.user": "uzanto",
"search_results.total": "{count, number} {count, plural, one {rezultato} other {rezultatoj}}",
"standalone.public_title": "Rigardeti…",
"status.cannot_reblog": "Tiun publikaĵon oni ne povas diskonigi",
"status.delete": "Forigi",
"status.embed": "Embed",
"status.embed": "Enmeti",
"status.favourite": "Favori",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.load_more": "Ŝargi plie",
"status.media_hidden": "Sonbildaĵo kaŝita",
"status.mention": "Mencii @{name}",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.more": "Pli",
"status.mute_conversation": "Silentigi konversacion",
"status.open": "Disfaldi statkonigon",
"status.pin": "Pingli al la profilo",
"status.reblog": "Diskonigi",
"status.reblogged_by": "{name} diskonigita",
"status.reblogged_by": "{name} diskonigis",
"status.reply": "Respondi",
"status.replyAll": "Reply to thread",
"status.report": "Report @{name}",
"status.replyAll": "Respondi al la fadeno",
"status.report": "Signali @{name}",
"status.sensitive_toggle": "Alklaki por vidi",
"status.sensitive_warning": "Tikla enhavo",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"status.share": "Diskonigi",
"status.show_less": "Refaldi",
"status.show_more": "Disfaldi",
"status.unmute_conversation": "Malsilentigi konversacion",
"status.unpin": "Depingli de profilo",
"tabs_bar.compose": "Ekskribi",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.federated_timeline": "Federacia tempolinio",
"tabs_bar.home": "Hejmo",
"tabs_bar.local_timeline": "Local",
"tabs_bar.local_timeline": "Loka tempolinio",
"tabs_bar.notifications": "Sciigoj",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Aldoni enhavaĵon",
"upload_form.description": "Describe for the visually impaired",
"upload_area.title": "Algliti por alŝuti",
"upload_button.label": "Aldoni sonbildaĵon",
"upload_form.description": "Priskribi por la misvidantaj",
"upload_form.undo": "Malfari",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound"
"upload_progress.label": "Alŝutanta…",
"video.close": "Fermi videon",
"video.exit_fullscreen": "Eliri el plenekrano",
"video.expand": "Vastigi videon",
"video.fullscreen": "Igi plenekrane",
"video.hide": "Kaŝi videon",
"video.mute": "Silentigi",
"video.pause": "Paŭzi",
"video.play": "Legi",
"video.unmute": "Malsilentigi"
}

View File

@@ -82,7 +82,6 @@
"empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
"empty_column.hashtag": "No hay nada en este hashtag aún.",
"empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
"empty_column.home.inactivity": "Tus notificaciones están vacías. Si has estado inactivo por un tiempo, se regenerará para ti pronto.",
"empty_column.home.public_timeline": "la línea de tiempo pública",
"empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
"empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.",
@@ -160,6 +159,11 @@
"privacy.public.short": "Público",
"privacy.unlisted.long": "No mostrar en la historia federada",
"privacy.unlisted.short": "Sin federar",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "ahora",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Cancelar",
"report.placeholder": "Comentarios adicionales",
"report.submit": "Publicar",
@@ -179,6 +183,7 @@
"status.load_more": "Cargar más",
"status.media_hidden": "Contenido multimedia oculto",
"status.mention": "Mencionar",
"status.more": "Más",
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estado",
"status.pin": "Fijar",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!",
"empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.",
"empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.",
"empty_column.home.inactivity": "فهرست پی‌گیری‌های شما خالی است. اگر مدتی است که غیرفعال بودید، این فهرست به زودی برایتان پر می‌شود.",
"empty_column.home.public_timeline": "فهرست نوشته‌های همه‌جا",
"empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشته‌های دیگران واکنش نشان دهید تا گفتگو آغاز شود.",
"empty_column.public": "این‌جا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا این‌جا پر شود",
@@ -160,6 +159,11 @@
"privacy.public.short": "عمومی",
"privacy.unlisted.long": "عمومی، ولی فهرست نکن",
"privacy.unlisted.short": "فهرست‌نشده",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "لغو",
"report.placeholder": "توضیح اضافه",
"report.submit": "بفرست",
@@ -179,6 +183,7 @@
"status.load_more": "بیشتر نشان بده",
"status.media_hidden": "تصویر پنهان شده",
"status.mention": "نام‌بردن از @{name}",
"status.more": "More",
"status.mute_conversation": "بی‌صداکردن گفتگو",
"status.open": "این نوشته را باز کن",
"status.pin": "نوشتهٔ ثابت نمایه",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -160,6 +159,11 @@
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not show in public timelines",
"privacy.unlisted.short": "Unlisted",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Peruuta",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
@@ -179,6 +183,7 @@
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Mainitse @{name}",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",

View File

@@ -63,7 +63,7 @@
"confirmations.mute.message": "Confirmez-vous le masquage de {name}?",
"confirmations.unfollow.confirm": "Ne plus suivre",
"confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name}?",
"embed.instructions": "Intégrez ce statut à votre site en copiant ce code ci-dessous.",
"embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.",
"embed.preview": "Il apparaîtra comme cela:",
"emoji_button.activity": "Activités",
"emoji_button.custom": "Personnalisés",
@@ -82,7 +82,6 @@
"empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir!",
"empty_column.hashtag": "Il ny a encore aucun contenu associé à ce hashtag",
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à dautres utilisateur⋅ice⋅s.",
"empty_column.home.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.",
"empty_column.home.public_timeline": "le fil public",
"empty_column.notifications": "Vous navez pas encore de notification. Interagissez avec dautres utilisateur⋅ice⋅s pour débuter la conversation.",
"empty_column.public": "Il ny a rien ici! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s dautres instances pour remplir le fil public.",
@@ -160,6 +159,11 @@
"privacy.public.short": "Public",
"privacy.unlisted.long": "Ne pas afficher dans les fils publics",
"privacy.unlisted.short": "Non-listé",
"relative_time.days": "{number} j",
"relative_time.hours": "{number} h",
"relative_time.just_now": "à linstant",
"relative_time.minutes": "{number} min",
"relative_time.seconds": "{number} s",
"reply_indicator.cancel": "Annuler",
"report.placeholder": "Commentaires additionnels",
"report.submit": "Envoyer",
@@ -179,6 +183,7 @@
"status.load_more": "Charger plus",
"status.media_hidden": "Média caché",
"status.mention": "Mentionner",
"status.more": "Plus",
"status.mute_conversation": "Masquer la conversation",
"status.open": "Déplier ce statut",
"status.pin": "Épingler sur le profil",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!",
"empty_column.hashtag": "אין כלום בהאשתג הזה עדיין.",
"empty_column.home": "אף אחד לא במעקב עדיין. אפשר לבקר ב{public} או להשתמש בחיפוש כדי להתחיל ולהכיר חצוצרנים אחרים.",
"empty_column.home.inactivity": "ציר זמן הבית שלך ריק. אם לא הייתה פעילות מצידך לאחרונה, הוא יתמלא מחדש בקרוב.",
"empty_column.home.public_timeline": "ציר זמן בין-קהילתי",
"empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב!",
"empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות.",
@@ -160,6 +159,11 @@
"privacy.public.short": "פומבי",
"privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים",
"privacy.unlisted.short": "לא לפיד הכללי",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "ביטול",
"report.placeholder": "הערות נוספות",
"report.submit": "שליחה",
@@ -179,6 +183,7 @@
"status.load_more": "עוד",
"status.media_hidden": "מדיה מוסתרת",
"status.mention": "פניה אל @{name}",
"status.more": "More",
"status.mute_conversation": "השתקת שיחה",
"status.open": "הרחבת הודעה",
"status.pin": "Pin on profile",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!",
"empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.",
"empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.",
"empty_column.home.inactivity": "Tvoj home feed je prazan. Ako si neko vrijeme bio neaktivan, uskoro ćese regenerirati.",
"empty_column.home.public_timeline": "javni timeline",
"empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.",
"empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio",
@@ -160,6 +159,11 @@
"privacy.public.short": "Javno",
"privacy.unlisted.long": "Ne prikazuj u javnim timelineovima",
"privacy.unlisted.short": "Unlisted",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Otkaži",
"report.placeholder": "Dodatni komentari",
"report.submit": "Pošalji",
@@ -179,6 +183,7 @@
"status.load_more": "Učitaj više",
"status.media_hidden": "Sakriven media sadržaj",
"status.mention": "Spomeni @{name}",
"status.more": "More",
"status.mute_conversation": "Utišaj razgovor",
"status.open": "Proširi ovaj status",
"status.pin": "Pin on profile",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
@@ -160,6 +159,11 @@
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not show in public timelines",
"privacy.unlisted.short": "Unlisted",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Mégsem",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
@@ -179,6 +183,7 @@
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Említés",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!",
"empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.",
"empty_column.home": "Anda sedang tidak mengikuti siapapun. Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.",
"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": "linimasa publik",
"empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.",
"empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisinya secara manual",
@@ -160,6 +159,11 @@
"privacy.public.short": "Publik",
"privacy.unlisted.long": "Tidak ditampilkan di linimasa publik",
"privacy.unlisted.short": "Tak Terdaftar",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Batal",
"report.placeholder": "Komentar tambahan",
"report.submit": "Kirim",
@@ -179,6 +183,7 @@
"status.load_more": "Tampilkan semua",
"status.media_hidden": "Media disembunyikan",
"status.mention": "Balasan @{name}",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Tampilkan status ini",
"status.pin": "Pin on profile",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "La lokala tempolineo esas vakua. Skribez ulo publike por iniciar la agiveso!",
"empty_column.hashtag": "Esas ankore nulo en ta gretovorto.",
"empty_column.home": "Tu sequas ankore nulu. Vizitez {public} od uzez la serchilo por komencar e renkontrar altra uzeri.",
"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": "la publika tempolineo",
"empty_column.notifications": "Tu havas ankore nula savigo. Komunikez kun altri por debutar la konverso.",
"empty_column.public": "Esas nulo hike! Skribez ulo publike, o manuale sequez uzeri de altra instaluri por plenigar ol.",
@@ -160,6 +159,11 @@
"privacy.public.short": "Publike",
"privacy.unlisted.long": "Ne montrar en publika tempolinei",
"privacy.unlisted.short": "Ne enlistigota",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Nihiligar",
"report.placeholder": "Plusa komenti",
"report.submit": "Sendar",
@@ -179,6 +183,7 @@
"status.load_more": "Kargar pluse",
"status.media_hidden": "Kontenajo celita",
"status.mention": "Mencionar @{name}",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Detaligar ca mesajo",
"status.pin": "Pin on profile",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!",
"empty_column.hashtag": "Non c'è ancora nessun post con questo hashtag.",
"empty_column.home": "Non stai ancora seguendo nessuno. Visita {public} o usa la ricerca per incontrare nuove persone.",
"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": "la timeline pubblica",
"empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.",
"empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.",
@@ -160,6 +159,11 @@
"privacy.public.short": "Pubblico",
"privacy.unlisted.long": "Non mostrare sulla timeline pubblica",
"privacy.unlisted.short": "Non elencato",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Annulla",
"report.placeholder": "Commenti aggiuntivi",
"report.submit": "Invia",
@@ -179,6 +183,7 @@
"status.load_more": "Mostra di più",
"status.media_hidden": "Allegato nascosto",
"status.mention": "Nomina @{name}",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Espandi questo post",
"status.pin": "Pin on profile",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
"empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
"empty_column.home.inactivity": "あなたのホームフィードにはなにもありません。あなたがしばらくの間アクティブではなかった場合はすぐ元通りになります。",
"empty_column.home.public_timeline": "連合タイムライン",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
@@ -184,6 +183,7 @@
"status.load_more": "もっと見る",
"status.media_hidden": "非表示のメディア",
"status.mention": "返信",
"status.more": "もっと見る",
"status.mute_conversation": "会話をミュート",
"status.open": "詳細を表示",
"status.pin": "プロフィールに固定表示",

View File

@@ -18,7 +18,7 @@
"account.unblock_domain": "{domain} 숨김 해제",
"account.unfollow": "팔로우 해제",
"account.unmute": "뮤트 해제",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "전체 프로필 보기",
"boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
@@ -33,7 +33,7 @@
"column.home": "홈",
"column.mutes": "뮤트 중인 사용자",
"column.notifications": "알림",
"column.pins": "고정된 Toot",
"column.pins": "고정된 ",
"column.public": "연합 타임라인",
"column_back_button.label": "돌아가기",
"column_header.hide_settings": "Hide settings",
@@ -47,7 +47,7 @@
"compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
"compose_form.lock_disclaimer.lock": "비공개",
"compose_form.placeholder": "지금 무엇을 하고 있나요?",
"compose_form.publish": "Toot",
"compose_form.publish": "",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "이 미디어를 민감한 미디어로 취급",
"compose_form.spoiler": "텍스트 숨기기",
@@ -63,8 +63,8 @@
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 퍼가세요.",
"embed.preview": "다음과 같이 표시됩니다:",
"emoji_button.activity": "활동",
"emoji_button.custom": "Custom",
"emoji_button.flags": "국기",
@@ -82,7 +82,6 @@
"empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",
"empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
"empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.",
"empty_column.home.inactivity": "홈 피드에 아무 것도 없습니다. 한동안 활동하지 않은 경우 곧 원래대로 돌아올 것입니다.",
"empty_column.home.public_timeline": "연합 타임라인",
"empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!",
"empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!",
@@ -113,7 +112,7 @@
"navigation_bar.info": "이 인스턴스에 대해서",
"navigation_bar.logout": "로그아웃",
"navigation_bar.mutes": "뮤트 중인 사용자",
"navigation_bar.pins": "고정된 Toot",
"navigation_bar.pins": "고정된 ",
"navigation_bar.preferences": "사용자 설정",
"navigation_bar.public_timeline": "연합 타임라인",
"notification.favourite": "{name}님이 즐겨찾기 했습니다",
@@ -159,29 +158,35 @@
"privacy.public.long": "공개 타임라인에 표시",
"privacy.public.short": "공개",
"privacy.unlisted.long": "공개 타임라인에 표시하지 않음",
"privacy.unlisted.short": "Unlisted",
"privacy.unlisted.short": "타임라인에 비표시",
"relative_time.days": "{number}일 전",
"relative_time.hours": "{number}시간 전",
"relative_time.just_now": "방금",
"relative_time.minutes": "{number}분 전",
"relative_time.seconds": "{number}초 전",
"reply_indicator.cancel": "취소",
"report.placeholder": "코멘트",
"report.submit": "신고하기",
"report.target": "문제가 된 사용자",
"search.placeholder": "검색",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
"search_popout.search_format": "고급 검색 방법",
"search_popout.tips.hashtag": "해시태그",
"search_popout.tips.status": "",
"search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
"search_popout.tips.user": "유저",
"search_results.total": "{count, number}건의 결과",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
"status.delete": "삭제",
"status.embed": "Embed",
"status.embed": "공유하기",
"status.favourite": "즐겨찾기",
"status.load_more": "더 보기",
"status.media_hidden": "미디어 숨겨짐",
"status.mention": "답장",
"status.more": "More",
"status.mute_conversation": "이 대화를 뮤트",
"status.open": "상세 정보 표시",
"status.pin": "Pin on profile",
"status.pin": "고정",
"status.reblog": "부스트",
"status.reblogged_by": "{name}님이 부스트 했습니다",
"status.reply": "답장",
@@ -193,7 +198,7 @@
"status.show_less": "숨기기",
"status.show_more": "더 보기",
"status.unmute_conversation": "이 대화의 뮤트 해제하기",
"status.unpin": "Unpin from profile",
"status.unpin": "고정 해제",
"tabs_bar.compose": "포스트",
"tabs_bar.federated_timeline": "연합",
"tabs_bar.home": "홈",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "De lokale tijdlijn is nog leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
"empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
"empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
"empty_column.home.inactivity": "Deze tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.",
"empty_column.home.public_timeline": "de globale tijdlijn",
"empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.",
"empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.",
@@ -160,6 +159,11 @@
"privacy.public.short": "Openbaar",
"privacy.unlisted.long": "Niet op openbare tijdlijnen tonen",
"privacy.unlisted.short": "Minder openbaar",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Annuleren",
"report.placeholder": "Extra opmerkingen",
"report.submit": "Verzenden",
@@ -179,6 +183,7 @@
"status.load_more": "Meer laden",
"status.media_hidden": "Media verborgen",
"status.mention": "Vermeld @{name}",
"status.more": "Meer",
"status.mute_conversation": "Negeer conversatie",
"status.open": "Toot volledig tonen",
"status.pin": "Aan profielpagina vastmaken",

View File

@@ -82,7 +82,6 @@
"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.",
"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": "en offentlig tidslinje",
"empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
"empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
@@ -160,6 +159,11 @@
"privacy.public.short": "Offentlig",
"privacy.unlisted.long": "Ikke vis i offentlige tidslinjer",
"privacy.unlisted.short": "Uoppført",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Avbryt",
"report.placeholder": "Tilleggskommentarer",
"report.submit": "Send inn",
@@ -179,6 +183,7 @@
"status.load_more": "Last mer",
"status.media_hidden": "Media skjult",
"status.mention": "Nevn @{name}",
"status.more": "More",
"status.mute_conversation": "Demp samtale",
"status.open": "Utvid denne statusen",
"status.pin": "Pin on profile",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir!",
"empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
"empty_column.home": "Vòstre flux dacuèlh es void. 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 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í! Escrivètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo flux public.",
@@ -160,6 +159,11 @@
"privacy.public.short": "Public",
"privacy.unlisted.long": "Mostrar pas dins los fluxes publics",
"privacy.unlisted.short": "Pas-listat",
"relative_time.days": "fa {number}j",
"relative_time.hours": "fa {number}h",
"relative_time.just_now": "ara",
"relative_time.minutes": "fa {number} minutas",
"relative_time.seconds": "fa {number} segondas",
"reply_indicator.cancel": "Anullar",
"report.placeholder": "Comentaris addicionals",
"report.submit": "Mandar",
@@ -179,6 +183,7 @@
"status.load_more": "Cargar mai",
"status.media_hidden": "Mèdia rescondut",
"status.mention": "Mencionar",
"status.more": "Mai",
"status.mute_conversation": "Rescondre la conversacion",
"status.open": "Desplegar aqueste estatut",
"status.pin": "Penjar al perfil",

View File

@@ -82,7 +82,6 @@
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
"empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
"empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
"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ć.",
@@ -160,6 +159,11 @@
"privacy.public.short": "Publiczny",
"privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczny",
"relative_time.days": "{number} dni",
"relative_time.hours": "{number} godz.",
"relative_time.just_now": "teraz",
"relative_time.minutes": "{number} min.",
"relative_time.seconds": "{number} s.",
"reply_indicator.cancel": "Anuluj",
"report.placeholder": "Dodatkowe komentarze",
"report.submit": "Wyślij",
@@ -179,6 +183,7 @@
"status.load_more": "Załaduj więcej",
"status.media_hidden": "Zawartość multimedialna ukryta",
"status.mention": "Wspomnij o @{name}",
"status.more": "Więcej",
"status.mute_conversation": "Wycisz konwersację",
"status.open": "Rozszerz ten wpis",
"status.pin": "Przypnij do profilu",

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