mirror of
https://github.com/yingziwu/mastodon.git
synced 2026-02-08 13:35:14 +00:00
upgrade to v2.9.0rc1
This commit is contained in:
commit
6fef22365d
495 changed files with 10757 additions and 5102 deletions
|
|
@ -30,8 +30,8 @@ plugins:
|
|||
channel: eslint-5
|
||||
rubocop:
|
||||
enabled: true
|
||||
channel: rubocop-0-54
|
||||
scss-lint:
|
||||
channel: rubocop-0-71
|
||||
sass-lint:
|
||||
enabled: true
|
||||
exclude_patterns:
|
||||
- spec/
|
||||
|
|
|
|||
10
.dependabot/config.yml
Normal file
10
.dependabot/config.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
version: 1
|
||||
|
||||
update_configs:
|
||||
- package_manager: "ruby:bundler"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
|
||||
- package_manager: "javascript"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
|
|
@ -10,6 +10,7 @@ DB_NAME=postgres
|
|||
DB_PASS=
|
||||
DB_PORT=5432
|
||||
# Optional ElasticSearch configuration
|
||||
# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set)
|
||||
# ES_ENABLED=true
|
||||
# ES_HOST=es
|
||||
# ES_PORT=9200
|
||||
|
|
|
|||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
patreon: mastodon
|
||||
open_collective: mastodon
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
require:
|
||||
- rubocop-rails
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.3
|
||||
Exclude:
|
||||
|
|
@ -80,7 +83,10 @@ Rails/HttpStatus:
|
|||
Rails/Exit:
|
||||
Exclude:
|
||||
- 'lib/mastodon/*'
|
||||
- 'lib/cli'
|
||||
- 'lib/cli.rb'
|
||||
|
||||
Rails/HelperInstanceVariable:
|
||||
Enabled: false
|
||||
|
||||
Style/ClassAndModuleChildren:
|
||||
Enabled: false
|
||||
|
|
|
|||
37
.sass-lint.yml
Normal file
37
.sass-lint.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Linter Documentation:
|
||||
# https://github.com/sasstools/sass-lint/tree/v1.13.1/docs/options
|
||||
|
||||
files:
|
||||
include: app/javascript/styles/**/*.scss
|
||||
ignore:
|
||||
- app/javascript/styles/mastodon/reset.scss
|
||||
|
||||
rules:
|
||||
# Disallows
|
||||
no-color-literals: 0
|
||||
no-css-comments: 0
|
||||
no-duplicate-properties: 0
|
||||
no-ids: 0
|
||||
no-important: 0
|
||||
no-mergeable-selectors: 0
|
||||
no-misspelled-properties: 0
|
||||
no-qualifying-elements: 0
|
||||
no-transition-all: 0
|
||||
no-vendor-prefixes: 0
|
||||
|
||||
# Nesting
|
||||
force-element-nesting: 0
|
||||
force-attribute-nesting: 0
|
||||
force-pseudo-nesting: 0
|
||||
|
||||
# Name Formats
|
||||
class-name-format: 0
|
||||
leading-zero: 0
|
||||
|
||||
# Style Guide
|
||||
attribute-quotes: 0
|
||||
hex-length: 0
|
||||
indentation: 0
|
||||
nesting-depth: 0
|
||||
property-sort-order: 0
|
||||
quotes: 0
|
||||
264
.scss-lint.yml
264
.scss-lint.yml
|
|
@ -1,264 +0,0 @@
|
|||
# Linter Documentation:
|
||||
# https://github.com/brigade/scss-lint/blob/v0.42.2/lib/scss_lint/linter/README.md
|
||||
|
||||
scss_files: 'app/javascript/styles/**/*.scss'
|
||||
|
||||
exclude:
|
||||
- app/javascript/styles/reset.scss
|
||||
|
||||
linters:
|
||||
# Reports when you use improper spacing around ! (the "bang") in !default,
|
||||
# !global, !important, and !optional flags.
|
||||
BangFormat:
|
||||
enabled: false
|
||||
|
||||
# Whether or not to prefer `border: 0` over `border: none`.
|
||||
BorderZero:
|
||||
enabled: false
|
||||
|
||||
# Reports when you define a rule set using a selector with chained classes
|
||||
# (a.k.a. adjoining classes).
|
||||
ChainedClasses:
|
||||
enabled: false
|
||||
|
||||
# Prefer hexadecimal color codes over color keywords.
|
||||
# (e.g. `color: green` is a color keyword)
|
||||
ColorKeyword:
|
||||
enabled: false
|
||||
|
||||
# Prefer color literals (keywords or hexadecimal codes) to be used only in
|
||||
# variable declarations. They should be referred to via variables everywhere
|
||||
# else.
|
||||
ColorVariable:
|
||||
enabled: true
|
||||
|
||||
# Which form of comments to prefer in CSS.
|
||||
Comment:
|
||||
enabled: false
|
||||
|
||||
# Reports @debug statements (which you probably left behind accidentally).
|
||||
DebugStatement:
|
||||
enabled: false
|
||||
|
||||
# Rule sets should be ordered as follows:
|
||||
# - @extend declarations
|
||||
# - @include declarations without inner @content
|
||||
# - properties, @include declarations with inner @content
|
||||
# - nested rule sets.
|
||||
DeclarationOrder:
|
||||
enabled: false
|
||||
|
||||
# `scss-lint:disable` control comments should be preceded by a comment
|
||||
# explaining why these linters are being disabled for this file.
|
||||
# See https://github.com/brigade/scss-lint#disabling-linters-via-source for
|
||||
# more information.
|
||||
DisableLinterReason:
|
||||
enabled: true
|
||||
|
||||
# Reports when you define the same property twice in a single rule set.
|
||||
DuplicateProperty:
|
||||
enabled: false
|
||||
|
||||
# Separate rule, function, and mixin declarations with empty lines.
|
||||
EmptyLineBetweenBlocks:
|
||||
enabled: true
|
||||
|
||||
# Reports when you have an empty rule set.
|
||||
EmptyRule:
|
||||
enabled: true
|
||||
|
||||
# Reports when you have an @extend directive.
|
||||
ExtendDirective:
|
||||
enabled: false
|
||||
|
||||
# Files should always have a final newline. This results in better diffs
|
||||
# when adding lines to the file, since SCM systems such as git won't
|
||||
# think that you touched the last line.
|
||||
FinalNewline:
|
||||
enabled: false
|
||||
|
||||
# HEX colors should use three-character values where possible.
|
||||
HexLength:
|
||||
enabled: false
|
||||
|
||||
# HEX color values should use lower-case colors to differentiate between
|
||||
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
|
||||
HexNotation:
|
||||
enabled: true
|
||||
|
||||
# Avoid using ID selectors.
|
||||
IdSelector:
|
||||
enabled: false
|
||||
|
||||
# The basenames of @imported SCSS partials should not begin with an
|
||||
# underscore and should not include the filename extension.
|
||||
ImportPath:
|
||||
enabled: false
|
||||
|
||||
# Avoid using !important in properties. It is usually indicative of a
|
||||
# misunderstanding of CSS specificity and can lead to brittle code.
|
||||
ImportantRule:
|
||||
enabled: false
|
||||
|
||||
# Indentation should always be done in increments of 2 spaces.
|
||||
Indentation:
|
||||
enabled: true
|
||||
width: 2
|
||||
|
||||
# Don't write leading zeros for numeric values with a decimal point.
|
||||
LeadingZero:
|
||||
enabled: false
|
||||
|
||||
# Reports when you define the same selector twice in a single sheet.
|
||||
MergeableSelector:
|
||||
enabled: false
|
||||
|
||||
# Functions, mixins, variables, and placeholders should be declared
|
||||
# with all lowercase letters and hyphens instead of underscores.
|
||||
NameFormat:
|
||||
enabled: false
|
||||
|
||||
# Avoid nesting selectors too deeply.
|
||||
NestingDepth:
|
||||
enabled: false
|
||||
|
||||
# Always use placeholder selectors in @extend.
|
||||
PlaceholderInExtend:
|
||||
enabled: false
|
||||
|
||||
# Sort properties in a strict order.
|
||||
PropertySortOrder:
|
||||
enabled: false
|
||||
|
||||
# Reports when you use an unknown or disabled CSS property
|
||||
# (ignoring vendor-prefixed properties).
|
||||
PropertySpelling:
|
||||
enabled: false
|
||||
|
||||
# Configure which units are allowed for property values.
|
||||
PropertyUnits:
|
||||
enabled: false
|
||||
|
||||
# Pseudo-elements, like ::before, and ::first-letter, should be declared
|
||||
# with two colons. Pseudo-classes, like :hover and :first-child, should
|
||||
# be declared with one colon.
|
||||
PseudoElement:
|
||||
enabled: true
|
||||
|
||||
# Avoid qualifying elements in selectors (also known as "tag-qualifying").
|
||||
QualifyingElement:
|
||||
enabled: false
|
||||
|
||||
# Don't write selectors with a depth of applicability greater than 3.
|
||||
SelectorDepth:
|
||||
enabled: false
|
||||
|
||||
# Selectors should always use hyphenated-lowercase, rather than camelCase or
|
||||
# snake_case.
|
||||
SelectorFormat:
|
||||
enabled: false
|
||||
convention: hyphenated_lowercase
|
||||
|
||||
# Prefer the shortest shorthand form possible for properties that support it.
|
||||
Shorthand:
|
||||
enabled: true
|
||||
|
||||
# Each property should have its own line, except in the special case of
|
||||
# single line rulesets.
|
||||
SingleLinePerProperty:
|
||||
enabled: true
|
||||
allow_single_line_rule_sets: true
|
||||
|
||||
# Split selectors onto separate lines after each comma, and have each
|
||||
# individual selector occupy a single line.
|
||||
SingleLinePerSelector:
|
||||
enabled: true
|
||||
|
||||
# Commas in lists should be followed by a space.
|
||||
SpaceAfterComma:
|
||||
enabled: false
|
||||
|
||||
# Properties should be formatted with a single space separating the colon
|
||||
# from the property's value.
|
||||
SpaceAfterPropertyColon:
|
||||
enabled: true
|
||||
|
||||
# Properties should be formatted with no space between the name and the
|
||||
# colon.
|
||||
SpaceAfterPropertyName:
|
||||
enabled: true
|
||||
|
||||
# Variables should be formatted with a single space separating the colon
|
||||
# from the variable's value.
|
||||
SpaceAfterVariableColon:
|
||||
enabled: true
|
||||
|
||||
# Variables should be formatted with no space between the name and the
|
||||
# colon.
|
||||
SpaceAfterVariableName:
|
||||
enabled: false
|
||||
|
||||
# Operators should be formatted with a single space on both sides of an
|
||||
# infix operator.
|
||||
SpaceAroundOperator:
|
||||
enabled: true
|
||||
|
||||
# Opening braces should be preceded by a single space.
|
||||
SpaceBeforeBrace:
|
||||
enabled: true
|
||||
|
||||
# Parentheses should not be padded with spaces.
|
||||
SpaceBetweenParens:
|
||||
enabled: false
|
||||
|
||||
# Enforces that string literals should be written with a consistent form
|
||||
# of quotes (single or double).
|
||||
StringQuotes:
|
||||
enabled: false
|
||||
|
||||
# Property values, @extend, @include, and @import directives, and variable
|
||||
# declarations should always end with a semicolon.
|
||||
TrailingSemicolon:
|
||||
enabled: true
|
||||
|
||||
# Reports lines containing trailing whitespace.
|
||||
TrailingWhitespace:
|
||||
enabled: true
|
||||
|
||||
# Don't write trailing zeros for numeric values with a decimal point.
|
||||
TrailingZero:
|
||||
enabled: false
|
||||
|
||||
# Don't use the `all` keyword to specify transition properties.
|
||||
TransitionAll:
|
||||
enabled: false
|
||||
|
||||
# Numeric values should not contain unnecessary fractional portions.
|
||||
UnnecessaryMantissa:
|
||||
enabled: false
|
||||
|
||||
# Do not use parent selector references (&) when they would otherwise
|
||||
# be unnecessary.
|
||||
UnnecessaryParentReference:
|
||||
enabled: false
|
||||
|
||||
# URLs should be valid and not contain protocols or domain names.
|
||||
UrlFormat:
|
||||
enabled: true
|
||||
|
||||
# URLs should always be enclosed within quotes.
|
||||
UrlQuotes:
|
||||
enabled: true
|
||||
|
||||
# Properties, like color and font, are easier to read and maintain
|
||||
# when defined using variables rather than literals.
|
||||
VariableForProperty:
|
||||
enabled: false
|
||||
|
||||
# Avoid vendor prefixes. Or rather: don't write them yourself.
|
||||
VendorPrefix:
|
||||
enabled: false
|
||||
|
||||
# Omit length units on zero values, e.g. `0px` vs. `0`.
|
||||
ZeroUnit:
|
||||
enabled: true
|
||||
|
|
@ -43,4 +43,4 @@ Gruntfile.js
|
|||
|
||||
# for specific ignore
|
||||
!.svgo.yml
|
||||
|
||||
!sass-lint/**/*.yml
|
||||
|
|
|
|||
283
AUTHORS.md
283
AUTHORS.md
|
|
@ -6,8 +6,8 @@ and provided thanks to the work of the following contributors:
|
|||
|
||||
* [Gargron](https://github.com/Gargron)
|
||||
* [ykzts](https://github.com/ykzts)
|
||||
* [akihikodaki](https://github.com/akihikodaki)
|
||||
* [ThibG](https://github.com/ThibG)
|
||||
* [akihikodaki](https://github.com/akihikodaki)
|
||||
* [mjankowski](https://github.com/mjankowski)
|
||||
* [dependabot[bot]](https://github.com/apps/dependabot)
|
||||
* [unarist](https://github.com/unarist)
|
||||
|
|
@ -27,14 +27,14 @@ and provided thanks to the work of the following contributors:
|
|||
* [blackle](https://github.com/blackle)
|
||||
* [Quent-in](https://github.com/Quent-in)
|
||||
* [JantsoP](https://github.com/JantsoP)
|
||||
* [mabkenar](https://github.com/mabkenar)
|
||||
* [Kjwon15](https://github.com/Kjwon15)
|
||||
* [mabkenar](https://github.com/mabkenar)
|
||||
* [nullkal](https://github.com/nullkal)
|
||||
* [yookoala](https://github.com/yookoala)
|
||||
* [shuheiktgw](https://github.com/shuheiktgw)
|
||||
* [ashfurrow](https://github.com/ashfurrow)
|
||||
* [Quenty31](https://github.com/Quenty31)
|
||||
* [zunda](https://github.com/zunda)
|
||||
* [Quenty31](https://github.com/Quenty31)
|
||||
* [eramdam](https://github.com/eramdam)
|
||||
* [takayamaki](https://github.com/takayamaki)
|
||||
* [masarakki](https://github.com/masarakki)
|
||||
|
|
@ -45,8 +45,8 @@ and provided thanks to the work of the following contributors:
|
|||
* [stephenburgess8](https://github.com/stephenburgess8)
|
||||
* [Wonderfall](https://github.com/Wonderfall)
|
||||
* [matteoaquila](https://github.com/matteoaquila)
|
||||
* [rkarabut](https://github.com/rkarabut)
|
||||
* [yukimochi](https://github.com/yukimochi)
|
||||
* [rkarabut](https://github.com/rkarabut)
|
||||
* [Artoria2e5](https://github.com/Artoria2e5)
|
||||
* [nightpool](https://github.com/nightpool)
|
||||
* [marrus-sh](https://github.com/marrus-sh)
|
||||
|
|
@ -64,11 +64,14 @@ and provided thanks to the work of the following contributors:
|
|||
* [MaciekBaron](https://github.com/MaciekBaron)
|
||||
* [MitarashiDango](mailto:mitarashidango@users.noreply.github.com)
|
||||
* [beatrix-bitrot](https://github.com/beatrix-bitrot)
|
||||
* [Aditoo17](https://github.com/Aditoo17)
|
||||
* [adbelle](https://github.com/adbelle)
|
||||
* [evanminto](https://github.com/evanminto)
|
||||
* [MightyPork](https://github.com/MightyPork)
|
||||
* [yhirano55](https://github.com/yhirano55)
|
||||
* [rinsuki](https://github.com/rinsuki)
|
||||
* [camponez](https://github.com/camponez)
|
||||
* [hinaloe](https://github.com/hinaloe)
|
||||
* [SerCom-KC](https://github.com/SerCom-KC)
|
||||
* [aschmitz](https://github.com/aschmitz)
|
||||
* [devkral](https://github.com/devkral)
|
||||
|
|
@ -81,10 +84,8 @@ and provided thanks to the work of the following contributors:
|
|||
* [lindwurm](https://github.com/lindwurm)
|
||||
* [victorhck](mailto:victorhck@geeko.site)
|
||||
* [voidsatisfaction](https://github.com/voidsatisfaction)
|
||||
* [rinsuki](https://github.com/rinsuki)
|
||||
* [hikari-no-yume](https://github.com/hikari-no-yume)
|
||||
* [angristan](https://github.com/angristan)
|
||||
* [hinaloe](https://github.com/hinaloe)
|
||||
* [seefood](https://github.com/seefood)
|
||||
* [jackjennings](https://github.com/jackjennings)
|
||||
* [spla](mailto:spla@mastodont.cat)
|
||||
|
|
@ -102,9 +103,10 @@ and provided thanks to the work of the following contributors:
|
|||
* [victorhck](https://github.com/victorhck)
|
||||
* [kedamaDQ](https://github.com/kedamaDQ)
|
||||
* [puckipedia](https://github.com/puckipedia)
|
||||
* [trwnh](https://github.com/trwnh)
|
||||
* [fvh-P](https://github.com/fvh-P)
|
||||
* [contraexemplo](https://github.com/contraexemplo)
|
||||
* [Aditoo17](https://github.com/Aditoo17)
|
||||
* [Anna e só](mailto:contraexemplos@gmail.com)
|
||||
* [BenLubar](https://github.com/BenLubar)
|
||||
* [kazu9su](https://github.com/kazu9su)
|
||||
* [Komic](https://github.com/Komic)
|
||||
* [lmorchard](https://github.com/lmorchard)
|
||||
|
|
@ -117,7 +119,6 @@ and provided thanks to the work of the following contributors:
|
|||
* [goofy-bz](mailto:goofy@babelzilla.org)
|
||||
* [kadiix](https://github.com/kadiix)
|
||||
* [kodacs](https://github.com/kodacs)
|
||||
* [trwnh](https://github.com/trwnh)
|
||||
* [JMendyk](https://github.com/JMendyk)
|
||||
* [KScl](https://github.com/KScl)
|
||||
* [sterdev](https://github.com/sterdev)
|
||||
|
|
@ -133,6 +134,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [Reverite](https://github.com/Reverite)
|
||||
* [JohnD28](https://github.com/JohnD28)
|
||||
* [znz](https://github.com/znz)
|
||||
* [marek-lach](https://github.com/marek-lach)
|
||||
* [Naouak](https://github.com/Naouak)
|
||||
* [pawelngei](https://github.com/pawelngei)
|
||||
* [rtucker](https://github.com/rtucker)
|
||||
|
|
@ -150,7 +152,6 @@ and provided thanks to the work of the following contributors:
|
|||
* [178inaba](https://github.com/178inaba)
|
||||
* [alyssais](https://github.com/alyssais)
|
||||
* [hiphref](https://github.com/hiphref)
|
||||
* [BenLubar](https://github.com/BenLubar)
|
||||
* [stalker314314](https://github.com/stalker314314)
|
||||
* [huertanix](https://github.com/huertanix)
|
||||
* [genesixx](https://github.com/genesixx)
|
||||
|
|
@ -161,16 +162,16 @@ and provided thanks to the work of the following contributors:
|
|||
* [kmichl](https://github.com/kmichl)
|
||||
* [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name)
|
||||
* [saper](https://github.com/saper)
|
||||
* [marek-lach](https://github.com/marek-lach)
|
||||
* [nevillepark](https://github.com/nevillepark)
|
||||
* [ornithocoder](https://github.com/ornithocoder)
|
||||
* [pierreozoux](https://github.com/pierreozoux)
|
||||
* [qguv](https://github.com/qguv)
|
||||
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
|
||||
* [sascha-sl](https://github.com/sascha-sl)
|
||||
* [harukasan](https://github.com/harukasan)
|
||||
* [stamak](https://github.com/stamak)
|
||||
* [Technowix](mailto:technowix@users.noreply.github.com)
|
||||
* [Eychics](https://github.com/Eychics)
|
||||
* [Zoeille](https://github.com/Zoeille)
|
||||
* [Thor Harald Johansen](mailto:thj@thj.no)
|
||||
* [0x70b1a5](https://github.com/0x70b1a5)
|
||||
* [gled-rs](https://github.com/gled-rs)
|
||||
|
|
@ -244,9 +245,9 @@ and provided thanks to the work of the following contributors:
|
|||
* [raymestalez](https://github.com/raymestalez)
|
||||
* [remram44](https://github.com/remram44)
|
||||
* [sts10](https://github.com/sts10)
|
||||
* [sascha-sl](https://github.com/sascha-sl)
|
||||
* [u1-liquid](https://github.com/u1-liquid)
|
||||
* [sim6](https://github.com/sim6)
|
||||
* [Sir-Boops](https://github.com/Sir-Boops)
|
||||
* [stemid](https://github.com/stemid)
|
||||
* [sumdog](https://github.com/sumdog)
|
||||
* [ThomasLeister](https://github.com/ThomasLeister)
|
||||
|
|
@ -316,8 +317,11 @@ and provided thanks to the work of the following contributors:
|
|||
* [Andreas Drop](mailto:andy@remline.de)
|
||||
* [andi1984](https://github.com/andi1984)
|
||||
* [schas002](https://github.com/schas002)
|
||||
* [contraexemplo](https://github.com/contraexemplo)
|
||||
* [abackstrom](https://github.com/abackstrom)
|
||||
* [armandfardeau](https://github.com/armandfardeau)
|
||||
* [jumbosushi](https://github.com/jumbosushi)
|
||||
* [aurelien-reeves](https://github.com/aurelien-reeves)
|
||||
* [ayumin](https://github.com/ayumin)
|
||||
* [BaptisteGelez](https://github.com/BaptisteGelez)
|
||||
* [bzg](https://github.com/bzg)
|
||||
|
|
@ -335,7 +339,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [Motoma](https://github.com/Motoma)
|
||||
* [chriswk](https://github.com/chriswk)
|
||||
* [csu](https://github.com/csu)
|
||||
* [clarcharr](https://github.com/clarcharr)
|
||||
* [clarfon](https://github.com/clarfon)
|
||||
* [kklleemm](https://github.com/kklleemm)
|
||||
* [colindean](https://github.com/colindean)
|
||||
* [dachinat](https://github.com/dachinat)
|
||||
|
|
@ -358,6 +362,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [eai04191](https://github.com/eai04191)
|
||||
* [d3vgru](https://github.com/d3vgru)
|
||||
* [Elizafox](https://github.com/Elizafox)
|
||||
* [enewhuis](https://github.com/enewhuis)
|
||||
* [ericblade](https://github.com/ericblade)
|
||||
* [mikoim](https://github.com/mikoim)
|
||||
* [espenronnevik](https://github.com/espenronnevik)
|
||||
|
|
@ -446,6 +451,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [mouse-reeve](https://github.com/mouse-reeve)
|
||||
* [Mozinet-fr](https://github.com/Mozinet-fr)
|
||||
* [lae](https://github.com/lae)
|
||||
* [nosada](https://github.com/nosada)
|
||||
* [Nanamachi](https://github.com/Nanamachi)
|
||||
* [orinthe](https://github.com/orinthe)
|
||||
* [NecroTechno](https://github.com/NecroTechno)
|
||||
|
|
@ -462,10 +468,11 @@ and provided thanks to the work of the following contributors:
|
|||
* [noppa](https://github.com/noppa)
|
||||
* [Otakan951](https://github.com/Otakan951)
|
||||
* [fahy](https://github.com/fahy)
|
||||
* [PatrickRWells](https://github.com/PatrickRWells)
|
||||
* [Pangoraw](https://github.com/Pangoraw)
|
||||
* [peterkeen](https://github.com/peterkeen)
|
||||
* [pgate](https://github.com/pgate)
|
||||
* [PatrickRWells](mailto:32802366+patrickrwells@users.noreply.github.com)
|
||||
* [Paul](mailto:naydex.mc+github@gmail.com)
|
||||
* [Pete Keen](mailto:pete@petekeen.net)
|
||||
* [Pierre-Morgan Gate](mailto:pgate@users.noreply.github.com)
|
||||
* [Ratmir Karabut](mailto:rkarabut@sfmodern.ru)
|
||||
* [Reto Kromer](mailto:retokromer@users.noreply.github.com)
|
||||
* [Rey Tucker](mailto:git@reytucker.us)
|
||||
* [Rob Watson](mailto:rfwatson@users.noreply.github.com)
|
||||
|
|
@ -488,7 +495,6 @@ and provided thanks to the work of the following contributors:
|
|||
* [Sho Kusano](mailto:rosylilly@aduca.org)
|
||||
* [Shouko Yu](mailto:imshouko@gmail.com)
|
||||
* [Sina Mashek](mailto:sina@mashek.xyz)
|
||||
* [Sir-Boops](mailto:admin@boops.me)
|
||||
* [Soshi Kato](mailto:mail@sossii.com)
|
||||
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com)
|
||||
* [Stanislas](mailto:angristan@pm.me)
|
||||
|
|
@ -555,12 +561,14 @@ and provided thanks to the work of the following contributors:
|
|||
* [karlyeurl](mailto:karl.yeurl@gmail.com)
|
||||
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
|
||||
* [kodai](mailto:shirafuta.kodai@gmail.com)
|
||||
* [koyu](mailto:me@koyu.space)
|
||||
* [kuro5hin](mailto:rusty@kuro5hin.org)
|
||||
* [luzpaz](mailto:luzpaz@users.noreply.github.com)
|
||||
* [maxypy](mailto:maxime@mpigou.fr)
|
||||
* [mhe](mailto:mail@marcus-herrmann.com)
|
||||
* [mike castleman](mailto:m@mlcastle.net)
|
||||
* [mimikun](mailto:dzdzble_effort_311@outlook.jp)
|
||||
* [mohemohe](mailto:mohemohe@users.noreply.github.com)
|
||||
* [mshrtkch](mailto:mshrtkch@users.noreply.github.com)
|
||||
* [muan](mailto:muan@github.com)
|
||||
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com)
|
||||
|
|
@ -599,243 +607,338 @@ This document is provided for informational purposes only. Since it is only upda
|
|||
|
||||
Following people have contributed to translation of Mastodon:
|
||||
|
||||
- **Albanian**
|
||||
- Besnik Bleta
|
||||
- Aditoo
|
||||
- **Arabic**
|
||||
- ButterflyOfFire
|
||||
- Aditoo
|
||||
- Amrz0
|
||||
- **Asturian**
|
||||
- ButterflyOfFire
|
||||
- Enol P.
|
||||
- Aditoo
|
||||
- **Basque**
|
||||
- Osoitz
|
||||
- Aditoo
|
||||
- Aitzol
|
||||
- ButterflyOfFire
|
||||
- Gorka Azkarate
|
||||
- Osoitz
|
||||
- Peru Iparragirre
|
||||
- Gorka Azkarate
|
||||
- **Bengali**
|
||||
- dxwc
|
||||
- **Bulgarian**
|
||||
- ButterflyOfFire
|
||||
- Aditoo
|
||||
- **Catalan**
|
||||
- spla
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Joan Montané
|
||||
- Jose Luis
|
||||
- spla
|
||||
- **Chinese (Hong Kong)**
|
||||
- ButterflyOfFire
|
||||
- Luzi Leung
|
||||
- Aditoo
|
||||
- **Chinese (Simplified)**
|
||||
- Allen Zhong
|
||||
- ButterflyOfFire
|
||||
- SerCom_KC
|
||||
- martialarts
|
||||
- Kaitian Xie
|
||||
- Aditoo
|
||||
- pan93412
|
||||
- **Chinese (Traditional)**
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- James58899
|
||||
- Jeff Huang
|
||||
- pan93412
|
||||
- S1ttidoe477
|
||||
- SHA265
|
||||
- Jeff Huang
|
||||
- **Corsican**
|
||||
- Alix D. R.
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- **Croatian**
|
||||
- ButterflyOfFire
|
||||
- Aditoo
|
||||
- **Czech**
|
||||
- ButterflyOfFire
|
||||
- Lorem Ipsum
|
||||
- Aditoo
|
||||
- Marek Ľach
|
||||
- ButterflyOfFire
|
||||
- **Danish**
|
||||
- ButterflyOfFire
|
||||
- Einhjeriar
|
||||
- Rasmus Sæderup
|
||||
- **Dutch**
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- **Dutch**
|
||||
- Albakham
|
||||
- ButterflyOfFire
|
||||
- Jelv
|
||||
- jeroenpraat
|
||||
- rscmbbng
|
||||
- Aditoo
|
||||
- Jelv
|
||||
- **English**
|
||||
- ButterflyOfFire
|
||||
- Renato "Lond" Cerqueira
|
||||
- **English (United Kingdom)**
|
||||
- Albakham
|
||||
- **Esperanto**
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Becci Cat
|
||||
- Jeong Arm
|
||||
- Martin Bodin
|
||||
- Mélanie Chauvel
|
||||
- Vanege
|
||||
- Martin Bodin
|
||||
- tuxayo/Victor Grousset
|
||||
- **Finnish**
|
||||
- ButterflyOfFire
|
||||
- Jonne Arjoranta
|
||||
- S Heija
|
||||
- Mikko Poussu
|
||||
- Taru Luojola
|
||||
- S Heija
|
||||
- Aditoo
|
||||
- Jonne Arjoranta
|
||||
- **French**
|
||||
- Alda Marteau-Hardi
|
||||
- Albakham
|
||||
- Alix D. R.
|
||||
- Baptiste Jonglez
|
||||
- ButterflyOfFire
|
||||
- Franck Paul
|
||||
- Jean-Baptiste Holcroft
|
||||
- codl
|
||||
- Leia
|
||||
- Alda Marteau-Hardi
|
||||
- Mélanie Chauvel
|
||||
- Paul Marques Mota
|
||||
- azenet
|
||||
- Olivier Humbert
|
||||
- Aditoo
|
||||
- Jonathan Chan
|
||||
- Letiteuf55
|
||||
- Martin Bodin
|
||||
- Mélanie Chauvel
|
||||
- Olivier Humbert
|
||||
- Paul Marques Mota
|
||||
- Sylvhem
|
||||
- Baptiste Jonglez
|
||||
- goofy-mdn
|
||||
- Jean-Baptiste Holcroft
|
||||
- Technowix
|
||||
- Thibaut Girka
|
||||
- Martin Bodin
|
||||
- Théodore
|
||||
- azenet
|
||||
- codl
|
||||
- Thibaut Girka
|
||||
- Franck Paul
|
||||
- Sylvhem
|
||||
- **Galician**
|
||||
- ButterflyOfFire
|
||||
- Xose M.
|
||||
- Aditoo
|
||||
- manequim
|
||||
- **Georgian**
|
||||
- ButterflyOfFire
|
||||
- Aditoo
|
||||
- **German**
|
||||
- Benedikt Geißler
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Daniel
|
||||
- Eugen Rochko
|
||||
- Koyu Berteon
|
||||
- Patrick Figel
|
||||
- Weblate Admin
|
||||
- averageunicorn
|
||||
- ePirat
|
||||
- koyu
|
||||
- Koyu Berteon
|
||||
- larsreineke
|
||||
- koyu
|
||||
- Austin Jones
|
||||
- lilo
|
||||
- Benedikt Geißler
|
||||
- ePirat
|
||||
- Eugen Rochko
|
||||
- Weblate Admin
|
||||
- Patrick Figel
|
||||
- **Greek**
|
||||
- Antonis
|
||||
- ButterflyOfFire
|
||||
- Dimitris Maroulidis
|
||||
- Antonis
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Konstantinos Grevenitis
|
||||
- **Hebrew**
|
||||
- ButterflyOfFire
|
||||
- Aditoo
|
||||
- Ira
|
||||
- Yaron Shahrabani
|
||||
- **Hungarian**
|
||||
- Adam Paszternak
|
||||
- ButterflyOfFire
|
||||
- Adam Paszternak
|
||||
- Aditoo
|
||||
- Tibike Miklós
|
||||
- **Ido**
|
||||
- ButterflyOfFire
|
||||
- Aditoo
|
||||
- **Indonesian**
|
||||
- Alfiana Sibuea
|
||||
- afachri
|
||||
- ButterflyOfFire
|
||||
- Dito Kurnia Pratama
|
||||
- Eirworks
|
||||
- afachri
|
||||
- Aditoo
|
||||
- Alfiana Sibuea
|
||||
- se7entime
|
||||
- **Irish**
|
||||
- Albakham
|
||||
- Kevin Houlihan
|
||||
- **Italian**
|
||||
- Alessandro Levati
|
||||
- Albakham
|
||||
- ButterflyOfFire
|
||||
- Marcin Mikołajczak
|
||||
- Aditoo
|
||||
- Giuseppe Pignataro
|
||||
- Stefano
|
||||
- **Japanese**
|
||||
- ButterflyOfFire
|
||||
- Kumasun Morino
|
||||
- Yamagishi Kazutoshi
|
||||
- Hinaloe
|
||||
- 小鳥遊まりあ
|
||||
- mayaeh
|
||||
- osapon
|
||||
- unarist
|
||||
- 小鳥遊まりあ
|
||||
- 森の子リスのミーコの大冒険
|
||||
- **Korean**
|
||||
- Kumasun Morino
|
||||
- Yamagishi Kazutoshi
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Jeong Arm
|
||||
- unarist
|
||||
- **Kazakh**
|
||||
- arshat
|
||||
- Aditoo
|
||||
- **Korean**
|
||||
- Aditoo
|
||||
- Jeong Arm
|
||||
- ButterflyOfFire
|
||||
- Minori Hiraoka
|
||||
- Yamagishi Kazutoshi
|
||||
- **Lithuanian**
|
||||
- Sarunas Medeikis
|
||||
- **Malay**
|
||||
- ButterflyOfFire
|
||||
- Muhammad Nur Hidayat (MNH48)
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- **Norwegian (old code)**
|
||||
- ButterflyOfFire
|
||||
- Espen Rønnevik
|
||||
- Aditoo
|
||||
- Tale
|
||||
- **Occitan**
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Maxenç
|
||||
- Quenti2
|
||||
- Quentí
|
||||
- Maxenç
|
||||
- **Persian**
|
||||
- ButterflyOfFire
|
||||
- Masoud Abkenar
|
||||
- **Polish**
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- **Polish**
|
||||
- Aditoo
|
||||
- Albakham
|
||||
- ButterflyOfFire
|
||||
- Jakub Mendyk
|
||||
- Marcin Mikołajczak
|
||||
- Marek Ľach
|
||||
- Stasiek Michalski
|
||||
- Marcin Mikołajczak
|
||||
- Jakub Mendyk
|
||||
- Marek Ľach
|
||||
- krkk
|
||||
- **Portuguese**
|
||||
- Albakham
|
||||
- João Pinheiro
|
||||
- manequim
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Hugo Gameiro
|
||||
- manequim
|
||||
- **Portuguese (Brazil)**
|
||||
- André Andrade
|
||||
- Aditoo
|
||||
- Albakham
|
||||
- Anna e só
|
||||
- ButterflyOfFire
|
||||
- Renato "Lond" Cerqueira
|
||||
- **Romanian**
|
||||
- André Andrade
|
||||
- ButterflyOfFire
|
||||
- **Romanian**
|
||||
- adrianbblk
|
||||
- ButterflyOfFire
|
||||
- Aditoo
|
||||
- **Russian**
|
||||
- Andrew Zyabin
|
||||
- Albakham
|
||||
- ButterflyOfFire
|
||||
- Evgeny Petrov
|
||||
- Aditoo
|
||||
- Павел Гастелло
|
||||
- Andrew Zyabin
|
||||
- Yaron Shahrabani
|
||||
- **Serbian**
|
||||
- Branko Kokanovic
|
||||
- Burekz Finezt
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- **Serbian (latin)**
|
||||
- ButterflyOfFire
|
||||
- Aditoo
|
||||
- **Slovak**
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Ivan Pleva
|
||||
- Lorem Ipsum
|
||||
- Marek Ľach
|
||||
- Peter
|
||||
- **Slovenian**
|
||||
- ButterflyOfFire
|
||||
- Kristijan Tkalec
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- **Spanish**
|
||||
- Angeles Broullón
|
||||
- Antón López
|
||||
- Albakham
|
||||
- ButterflyOfFire
|
||||
- Carlos Mondragon
|
||||
- Antón López
|
||||
- Max Winkler
|
||||
- Pablo de la Concepción Sanz
|
||||
- Sergio Soriano
|
||||
- Angeles Broullón
|
||||
- Lothar Wolf
|
||||
- Aditoo
|
||||
- David Charte
|
||||
- Emmanuel
|
||||
- Lothar Wolf
|
||||
- Pablo de la Concepción Sanz
|
||||
- **Swedish**
|
||||
- ButterflyOfFire
|
||||
- Elias Mårtenson
|
||||
- Isak Holmström
|
||||
- Shellkr
|
||||
- Aditoo
|
||||
- Elias Mårtenson
|
||||
- Stefan Midjich
|
||||
- Tim Stahel
|
||||
- Jonas Hultén
|
||||
- **Telugu**
|
||||
- avndp
|
||||
- Ranjith Tellakula
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Joseph Nuthalapati
|
||||
- Ranjith Tellakula
|
||||
- avndp
|
||||
- **Thai**
|
||||
- ButterflyOfFire
|
||||
- parnikkapore
|
||||
- Thai Localization
|
||||
- Aditoo
|
||||
- **Turkish**
|
||||
- Ali Demirtas
|
||||
- ButterflyOfFire
|
||||
- Aditoo
|
||||
- **Ukrainian**
|
||||
- ButterflyOfFire
|
||||
- Ivan Verchenko
|
||||
- alexcleac
|
||||
- **Welsh**
|
||||
- ButterflyOfFire
|
||||
- Jaz-Michael King
|
||||
- Kevin Beynon
|
||||
- Owain Rhys Lewis
|
||||
- Renato "Lond" Cerqueira
|
||||
- Rhoslyn Prys
|
||||
- Aditoo
|
||||
- Ivan Verchenko
|
||||
- **Welsh**
|
||||
- carl morris
|
||||
- Jaz-Michael King
|
||||
- Owain Rhys Lewis
|
||||
- Rhoslyn Prys
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Renato "Lond" Cerqueira
|
||||
- Albakham
|
||||
- Kevin Beynon
|
||||
- **Armenian**
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- **Latvian**
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Maigonis
|
||||
- **Tamil**
|
||||
- Aditoo
|
||||
- ButterflyOfFire
|
||||
- Prasanna Venkadesh
|
||||
|
|
|
|||
218
CHANGELOG.md
218
CHANGELOG.md
|
|
@ -3,6 +3,224 @@ Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
|
||||
- **Add single-column mode in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/10807), [Gargron](https://github.com/tootsuite/mastodon/pull/10848), [Gargron](https://github.com/tootsuite/mastodon/pull/11003), [Gargron](https://github.com/tootsuite/mastodon/pull/10961), [Hanage999](https://github.com/tootsuite/mastodon/pull/10915), [noellabo](https://github.com/tootsuite/mastodon/pull/10917), [abcang](https://github.com/tootsuite/mastodon/pull/10859), [Gargron](https://github.com/tootsuite/mastodon/pull/10820), [Gargron](https://github.com/tootsuite/mastodon/pull/10835), [Gargron](https://github.com/tootsuite/mastodon/pull/10809), [Gargron](https://github.com/tootsuite/mastodon/pull/10963), [noellabo](https://github.com/tootsuite/mastodon/pull/10883), [Hanage999](https://github.com/tootsuite/mastodon/pull/10839))
|
||||
- Add waiting time to the list of pending accounts in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10985))
|
||||
- Add a keyboard shortcut to hide/show media in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10647), [Gargron](https://github.com/tootsuite/mastodon/pull/10838), [ThibG](https://github.com/tootsuite/mastodon/pull/10872))
|
||||
- Add `account_id` param to `GET /api/v1/notifications` ([pwoolcoc](https://github.com/tootsuite/mastodon/pull/10796))
|
||||
- Add confirmation modal for unboosting toots in web UI ([aurelien-reeves](https://github.com/tootsuite/mastodon/pull/10287))
|
||||
- Add emoji suggestions to content warning and poll option fields in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10555))
|
||||
- Add `source` attribute to response of `DELETE /api/v1/statuses/:id` ([ThibG](https://github.com/tootsuite/mastodon/pull/10669))
|
||||
- Add some caching for HTML versions of public status pages ([ThibG](https://github.com/tootsuite/mastodon/pull/10701))
|
||||
|
||||
### Changed
|
||||
|
||||
- **Change default layout to single column in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/10847))
|
||||
- **Change light theme** ([Gargron](https://github.com/tootsuite/mastodon/pull/10992), [Gargron](https://github.com/tootsuite/mastodon/pull/10996), [yuzulabo](https://github.com/tootsuite/mastodon/pull/10754), [Gargron](https://github.com/tootsuite/mastodon/pull/10845))
|
||||
- **Change preferences page into appearance, notifications, and other** ([Gargron](https://github.com/tootsuite/mastodon/pull/10977), [Gargron](https://github.com/tootsuite/mastodon/pull/10988))
|
||||
- Change priority of delete activity forwards for replies and reblogs ([Gargron](https://github.com/tootsuite/mastodon/pull/11002))
|
||||
- Change Mastodon logo to use primary text color of the given theme ([Gargron](https://github.com/tootsuite/mastodon/pull/10994))
|
||||
- Change reblogs counter to be updated when boosted privately ([Gargron](https://github.com/tootsuite/mastodon/pull/10964))
|
||||
- Change bio limit from 160 to 500 characters ([trwnh](https://github.com/tootsuite/mastodon/pull/10790))
|
||||
- Change API rate limiting to reduce allowed unauthenticated requests ([ThibG](https://github.com/tootsuite/mastodon/pull/10860), [hinaloe](https://github.com/tootsuite/mastodon/pull/10868), [mayaeh](https://github.com/tootsuite/mastodon/pull/10867))
|
||||
- Change help text of `tootctl emoji import` command to specify a gzipped TAR archive is required ([dariusk](https://github.com/tootsuite/mastodon/pull/11000))
|
||||
- Change web UI to hide poll options behind content warnings ([ThibG](https://github.com/tootsuite/mastodon/pull/10983))
|
||||
- Change silencing to ensure local effects and remote effects are the same for silenced local users ([ThibG](https://github.com/tootsuite/mastodon/pull/10575))
|
||||
- Change `tootctl domains purge` to remove custom emoji as well ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10721))
|
||||
- Change Docker image to keep `apt` working ([SuperSandro2000](https://github.com/tootsuite/mastodon/pull/10830))
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove `dist-upgrade` from Docker image ([SuperSandro2000](https://github.com/tootsuite/mastodon/pull/10822))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix RTL layout not being RTL within the columns area in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10990))
|
||||
- Fix display of alternative text when a media attachment is not available in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10981))
|
||||
- Fix not being able to directly switch between list timelines in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10973))
|
||||
- Fix media sensitivity not being maintained in delete & redraft in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10980))
|
||||
- Fix emoji picker being always displayed in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/10979), [yuzulabo](https://github.com/tootsuite/mastodon/pull/10801), [wcpaez](https://github.com/tootsuite/mastodon/pull/10978))
|
||||
- Fix potential private status leak through caching ([ThibG](https://github.com/tootsuite/mastodon/pull/10969))
|
||||
- Fix refreshing featured toots when the new collection is empty in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10971))
|
||||
- Fix undoing domain block also undoing individual moderation on users from before the domain block ([ThibG](https://github.com/tootsuite/mastodon/pull/10660))
|
||||
- Fix time not being local in the audit log ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10751))
|
||||
- Fix statuses removed by moderation re-appearing on subsequent fetches ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10732))
|
||||
- Fix misattribution of inlined announces if `attributedTo` isn't present in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/10967))
|
||||
- Fix `GET /api/v1/polls/:id` not requiring authentication for non-public polls ([Gargron](https://github.com/tootsuite/mastodon/pull/10960))
|
||||
- Fix handling of blank poll options in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/10946))
|
||||
- Fix avatar preview aspect ratio on edit profile page ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10931))
|
||||
- Fix web push notifications not being sent for polls ([ThibG](https://github.com/tootsuite/mastodon/pull/10864))
|
||||
- Fix cut off letters in last paragraph of statuses in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/10821))
|
||||
|
||||
## [2.8.4] - 2019-05-24
|
||||
### Fixed
|
||||
|
||||
- Fix delivery not retrying on some inbox errors that should be retriable ([ThibG](https://github.com/tootsuite/mastodon/pull/10812))
|
||||
- Fix unnecessary 5 minute cooldowns on signature verifications in some cases ([ThibG](https://github.com/tootsuite/mastodon/pull/10813))
|
||||
- Fix possible race condition when processing statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10815))
|
||||
|
||||
### Security
|
||||
|
||||
- Require specific OAuth scopes for specific endpoints of the streaming API, instead of merely requiring a token for all endpoints, and allow using WebSockets protocol negotiation to specify the access token instead of using a query string ([ThibG](https://github.com/tootsuite/mastodon/pull/10818))
|
||||
|
||||
## [2.8.3] - 2019-05-19
|
||||
### Added
|
||||
|
||||
- Add `og:image:alt` OpenGraph tag ([BenLubar](https://github.com/tootsuite/mastodon/pull/10779))
|
||||
- Add clickable area below avatar in statuses in web UI ([Dar13](https://github.com/tootsuite/mastodon/pull/10766))
|
||||
- Add crossed-out eye icon on account gallery in web UI ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10715))
|
||||
- Add media description tooltip to thumbnails in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10713))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change "mark as sensitive" button into a checkbox for clarity ([ThibG](https://github.com/tootsuite/mastodon/pull/10748))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix bug allowing users to publicly boost their private statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10775), [ThibG](https://github.com/tootsuite/mastodon/pull/10783))
|
||||
- Fix performance in formatter by a little ([ThibG](https://github.com/tootsuite/mastodon/pull/10765))
|
||||
- Fix some colors in the light theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10754))
|
||||
- Fix some colors of the high contrast theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10711))
|
||||
- Fix ambivalent active state of poll refresh button in web UI ([MaciekBaron](https://github.com/tootsuite/mastodon/pull/10720))
|
||||
- Fix duplicate posting being possible from web UI ([hinaloe](https://github.com/tootsuite/mastodon/pull/10785))
|
||||
- Fix "invited by" not showing up in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10791))
|
||||
|
||||
## [2.8.2] - 2019-05-05
|
||||
### Added
|
||||
|
||||
- Add `SOURCE_TAG` environment variable ([ushitora-anqou](https://github.com/tootsuite/mastodon/pull/10698))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix cropped hero image on frontpage ([BaptisteGelez](https://github.com/tootsuite/mastodon/pull/10702))
|
||||
- Fix blurhash gem not compiling on some operating systems ([Gargron](https://github.com/tootsuite/mastodon/pull/10700))
|
||||
- Fix unexpected CSS animations in some browsers ([ThibG](https://github.com/tootsuite/mastodon/pull/10699))
|
||||
- Fix closing video modal scrolling timelines to top ([ThibG](https://github.com/tootsuite/mastodon/pull/10695))
|
||||
|
||||
## [2.8.1] - 2019-05-04
|
||||
### Added
|
||||
|
||||
- Add link to existing domain block when trying to block an already-blocked domain ([ThibG](https://github.com/tootsuite/mastodon/pull/10663))
|
||||
- Add button to view context to media modal when opened from account gallery in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10676))
|
||||
- Add ability to create multiple-choice polls in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10603))
|
||||
- Add `GITHUB_REPOSITORY` and `SOURCE_BASE_URL` environment variables ([rosylilly](https://github.com/tootsuite/mastodon/pull/10600))
|
||||
- Add `/interact/` paths to `robots.txt` ([ThibG](https://github.com/tootsuite/mastodon/pull/10666))
|
||||
- Add `blurhash` to the Attachment entity in the REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/10630))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change hidden media to be shown as a blurhash-based colorful gradient instead of a black box in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10630))
|
||||
- Change rejected media to be shown as a blurhash-based gradient instead of a list of filenames in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10630))
|
||||
- Change e-mail whitelist/blacklist to not be checked when invited ([Gargron](https://github.com/tootsuite/mastodon/pull/10683))
|
||||
- Change cache header of REST API results to no-cache ([ThibG](https://github.com/tootsuite/mastodon/pull/10655))
|
||||
- Change the "mark media as sensitive" button to be more obvious in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10673), [Gargron](https://github.com/tootsuite/mastodon/pull/10682))
|
||||
- Change account gallery in web UI to display 3 columns, open media modal ([Gargron](https://github.com/tootsuite/mastodon/pull/10667), [Gargron](https://github.com/tootsuite/mastodon/pull/10674))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix LDAP/PAM/SAML/CAS users not being pre-approved ([Gargron](https://github.com/tootsuite/mastodon/pull/10621))
|
||||
- Fix accounts created through tootctl not being always pre-approved ([Gargron](https://github.com/tootsuite/mastodon/pull/10684))
|
||||
- Fix Sidekiq retrying ActivityPub processing jobs that fail validation ([ThibG](https://github.com/tootsuite/mastodon/pull/10614))
|
||||
- Fix toots not being scrolled into view sometimes through keyboard selection ([ThibG](https://github.com/tootsuite/mastodon/pull/10593))
|
||||
- Fix expired invite links being usable to bypass approval mode ([ThibG](https://github.com/tootsuite/mastodon/pull/10657))
|
||||
- Fix not being able to save e-mail preference for new pending accounts ([Gargron](https://github.com/tootsuite/mastodon/pull/10622))
|
||||
- Fix upload progressbar when image resizing is involved ([ThibG](https://github.com/tootsuite/mastodon/pull/10632))
|
||||
- Fix block action not automatically cancelling pending follow request ([ThibG](https://github.com/tootsuite/mastodon/pull/10633))
|
||||
- Fix stoplight logging to stderr separate from Rails logger ([Gargron](https://github.com/tootsuite/mastodon/pull/10624))
|
||||
- Fix sign up button not saying sign up when invite is used ([Gargron](https://github.com/tootsuite/mastodon/pull/10623))
|
||||
- Fix health checks in Docker Compose configuration ([fabianonline](https://github.com/tootsuite/mastodon/pull/10553))
|
||||
- Fix modal items not being scrollable on touch devices ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/10605))
|
||||
- Fix Keybase configuration using wrong domain when a web domain is used ([BenLubar](https://github.com/tootsuite/mastodon/pull/10565))
|
||||
- Fix avatar GIFs not being animated on-hover on public profiles ([hyenagirl64](https://github.com/tootsuite/mastodon/pull/10549))
|
||||
- Fix OpenGraph parser not understanding some valid property meta tags ([da2x](https://github.com/tootsuite/mastodon/pull/10604))
|
||||
- Fix wrong fonts being displayed when Roboto is installed on user's machine ([ThibG](https://github.com/tootsuite/mastodon/pull/10594))
|
||||
- Fix confirmation modals being too narrow for a secondary action button ([ThibG](https://github.com/tootsuite/mastodon/pull/10586))
|
||||
|
||||
## [2.8.0] - 2019-04-10
|
||||
### Added
|
||||
|
||||
- Add polls ([Gargron](https://github.com/tootsuite/mastodon/pull/10111), [ThibG](https://github.com/tootsuite/mastodon/pull/10155), [Gargron](https://github.com/tootsuite/mastodon/pull/10184), [ThibG](https://github.com/tootsuite/mastodon/pull/10196), [Gargron](https://github.com/tootsuite/mastodon/pull/10248), [ThibG](https://github.com/tootsuite/mastodon/pull/10255), [ThibG](https://github.com/tootsuite/mastodon/pull/10322), [Gargron](https://github.com/tootsuite/mastodon/pull/10138), [Gargron](https://github.com/tootsuite/mastodon/pull/10139), [Gargron](https://github.com/tootsuite/mastodon/pull/10144), [Gargron](https://github.com/tootsuite/mastodon/pull/10145),[Gargron](https://github.com/tootsuite/mastodon/pull/10146), [Gargron](https://github.com/tootsuite/mastodon/pull/10148), [Gargron](https://github.com/tootsuite/mastodon/pull/10151), [ThibG](https://github.com/tootsuite/mastodon/pull/10150), [Gargron](https://github.com/tootsuite/mastodon/pull/10168), [Gargron](https://github.com/tootsuite/mastodon/pull/10165), [Gargron](https://github.com/tootsuite/mastodon/pull/10172), [Gargron](https://github.com/tootsuite/mastodon/pull/10170), [Gargron](https://github.com/tootsuite/mastodon/pull/10171), [Gargron](https://github.com/tootsuite/mastodon/pull/10186), [Gargron](https://github.com/tootsuite/mastodon/pull/10189), [ThibG](https://github.com/tootsuite/mastodon/pull/10200), [rinsuki](https://github.com/tootsuite/mastodon/pull/10203), [Gargron](https://github.com/tootsuite/mastodon/pull/10213), [Gargron](https://github.com/tootsuite/mastodon/pull/10246), [Gargron](https://github.com/tootsuite/mastodon/pull/10265), [Gargron](https://github.com/tootsuite/mastodon/pull/10261), [ThibG](https://github.com/tootsuite/mastodon/pull/10333), [Gargron](https://github.com/tootsuite/mastodon/pull/10352), [ThibG](https://github.com/tootsuite/mastodon/pull/10140), [ThibG](https://github.com/tootsuite/mastodon/pull/10142), [ThibG](https://github.com/tootsuite/mastodon/pull/10141), [ThibG](https://github.com/tootsuite/mastodon/pull/10162), [ThibG](https://github.com/tootsuite/mastodon/pull/10161), [ThibG](https://github.com/tootsuite/mastodon/pull/10158), [ThibG](https://github.com/tootsuite/mastodon/pull/10156), [ThibG](https://github.com/tootsuite/mastodon/pull/10160), [Gargron](https://github.com/tootsuite/mastodon/pull/10185), [Gargron](https://github.com/tootsuite/mastodon/pull/10188), [ThibG](https://github.com/tootsuite/mastodon/pull/10195), [ThibG](https://github.com/tootsuite/mastodon/pull/10208), [Gargron](https://github.com/tootsuite/mastodon/pull/10187), [ThibG](https://github.com/tootsuite/mastodon/pull/10214), [ThibG](https://github.com/tootsuite/mastodon/pull/10209))
|
||||
- Add follows & followers managing UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10268), [Gargron](https://github.com/tootsuite/mastodon/pull/10308), [Gargron](https://github.com/tootsuite/mastodon/pull/10404), [Gargron](https://github.com/tootsuite/mastodon/pull/10293))
|
||||
- Add identity proof integration with Keybase ([Gargron](https://github.com/tootsuite/mastodon/pull/10297), [xgess](https://github.com/tootsuite/mastodon/pull/10375), [Gargron](https://github.com/tootsuite/mastodon/pull/10338), [Gargron](https://github.com/tootsuite/mastodon/pull/10350), [Gargron](https://github.com/tootsuite/mastodon/pull/10414))
|
||||
- Add option to overwrite imported data instead of merging ([Gargron](https://github.com/tootsuite/mastodon/pull/9962))
|
||||
- Add featured hashtags to profiles ([Gargron](https://github.com/tootsuite/mastodon/pull/9755), [Gargron](https://github.com/tootsuite/mastodon/pull/10167), [Gargron](https://github.com/tootsuite/mastodon/pull/10249), [ThibG](https://github.com/tootsuite/mastodon/pull/10034))
|
||||
- Add admission-based registrations mode ([Gargron](https://github.com/tootsuite/mastodon/pull/10250), [ThibG](https://github.com/tootsuite/mastodon/pull/10269), [Gargron](https://github.com/tootsuite/mastodon/pull/10264), [ThibG](https://github.com/tootsuite/mastodon/pull/10321), [Gargron](https://github.com/tootsuite/mastodon/pull/10349), [Gargron](https://github.com/tootsuite/mastodon/pull/10469))
|
||||
- Add support for WebP uploads ([acid-chicken](https://github.com/tootsuite/mastodon/pull/9879))
|
||||
- Add "copy link" item to status action bars in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/9983))
|
||||
- Add list title editing in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/9748))
|
||||
- Add a "Block & Report" button to the block confirmation dialog in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10360))
|
||||
- Add disappointed elephant when the page crashes in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10275))
|
||||
- Add ability to upload multiple files at once in web UI ([tmm576](https://github.com/tootsuite/mastodon/pull/9856))
|
||||
- Add indication when you are not allowed to follow an account in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10420), [Gargron](https://github.com/tootsuite/mastodon/pull/10491))
|
||||
- Add validations to admin settings to catch common mistakes ([Gargron](https://github.com/tootsuite/mastodon/pull/10348), [ThibG](https://github.com/tootsuite/mastodon/pull/10354))
|
||||
- Add `type`, `limit`, `offset`, `min_id`, `max_id`, `account_id` to search API ([Gargron](https://github.com/tootsuite/mastodon/pull/10091))
|
||||
- Add a preferences API so apps can share basic behaviours ([Gargron](https://github.com/tootsuite/mastodon/pull/10109))
|
||||
- Add `visibility` param to reblog REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/9851), [ThibG](https://github.com/tootsuite/mastodon/pull/10302))
|
||||
- Add `allowfullscreen` attribute to OEmbed iframe ([rinsuki](https://github.com/tootsuite/mastodon/pull/10370))
|
||||
- Add `blocked_by` relationship to the REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/10373))
|
||||
- Add `tootctl statuses remove` to sweep unreferenced statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/10063))
|
||||
- Add `tootctl search deploy` to avoid ugly rake task syntax ([Gargron](https://github.com/tootsuite/mastodon/pull/10403))
|
||||
- Add `tootctl self-destruct` to shut down server gracefully ([Gargron](https://github.com/tootsuite/mastodon/pull/10367))
|
||||
- Add option to hide application used to toot ([ThibG](https://github.com/tootsuite/mastodon/pull/9897), [rinsuki](https://github.com/tootsuite/mastodon/pull/9994), [hinaloe](https://github.com/tootsuite/mastodon/pull/10086))
|
||||
- Add `DB_SSLMODE` configuration variable ([sascha-sl](https://github.com/tootsuite/mastodon/pull/10210))
|
||||
- Add click-to-copy UI to invites page ([Gargron](https://github.com/tootsuite/mastodon/pull/10259))
|
||||
- Add self-replies fetching ([ThibG](https://github.com/tootsuite/mastodon/pull/10106), [ThibG](https://github.com/tootsuite/mastodon/pull/10128), [ThibG](https://github.com/tootsuite/mastodon/pull/10175), [ThibG](https://github.com/tootsuite/mastodon/pull/10201))
|
||||
- Add rate limit for media proxy requests ([Gargron](https://github.com/tootsuite/mastodon/pull/10490))
|
||||
- Add `tootctl emoji purge` ([Gargron](https://github.com/tootsuite/mastodon/pull/10481))
|
||||
- Add `tootctl accounts approve` ([Gargron](https://github.com/tootsuite/mastodon/pull/10480))
|
||||
- Add `tootctl accounts reset-relationships` ([noellabo](https://github.com/tootsuite/mastodon/pull/10483))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change design of landing page ([Gargron](https://github.com/tootsuite/mastodon/pull/10232), [Gargron](https://github.com/tootsuite/mastodon/pull/10260), [ThibG](https://github.com/tootsuite/mastodon/pull/10284), [ThibG](https://github.com/tootsuite/mastodon/pull/10291), [koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/10356), [Gargron](https://github.com/tootsuite/mastodon/pull/10245))
|
||||
- Change design of profile column in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10337), [Aditoo17](https://github.com/tootsuite/mastodon/pull/10387), [ThibG](https://github.com/tootsuite/mastodon/pull/10390), [mayaeh](https://github.com/tootsuite/mastodon/pull/10379), [ThibG](https://github.com/tootsuite/mastodon/pull/10411))
|
||||
- Change language detector threshold from 140 characters to 4 words ([Gargron](https://github.com/tootsuite/mastodon/pull/10376))
|
||||
- Change language detector to always kick in for non-latin alphabets ([Gargron](https://github.com/tootsuite/mastodon/pull/10276))
|
||||
- Change icons of features on admin dashboard ([Gargron](https://github.com/tootsuite/mastodon/pull/10366))
|
||||
- Change DNS timeouts from 1s to 5s ([ThibG](https://github.com/tootsuite/mastodon/pull/10238))
|
||||
- Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/tootsuite/mastodon/pull/10100), [BenLubar](https://github.com/tootsuite/mastodon/pull/10212))
|
||||
- Change public pages to be cacheable by proxies ([BenLubar](https://github.com/tootsuite/mastodon/pull/9059))
|
||||
- Change the 410 gone response for suspended accounts to be cacheable by proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/10339))
|
||||
- Change web UI to not not empty timeline of blocked users on block ([ThibG](https://github.com/tootsuite/mastodon/pull/10359))
|
||||
- Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/tootsuite/mastodon/pull/10378))
|
||||
- Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/tootsuite/mastodon/pull/9924))
|
||||
- Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/tootsuite/mastodon/pull/10289))
|
||||
- Change web UI to use new Web Share Target API ([gol-cha](https://github.com/tootsuite/mastodon/pull/9963))
|
||||
- Change ActivityPub reports to have persistent URIs ([ThibG](https://github.com/tootsuite/mastodon/pull/10303))
|
||||
- Change `tootctl accounts cull --dry-run` to list accounts that would be deleted ([BenLubar](https://github.com/tootsuite/mastodon/pull/10460))
|
||||
- Change format of CSV exports of follows and mutes to include extra settings ([ThibG](https://github.com/tootsuite/mastodon/pull/10495), [ThibG](https://github.com/tootsuite/mastodon/pull/10335))
|
||||
- Change ActivityPub collections to be cacheable by proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/10467))
|
||||
- Change REST API and public profiles to not return follows/followers for users that have blocked you ([Gargron](https://github.com/tootsuite/mastodon/pull/10491))
|
||||
- Change the groupings of menu items in settings navigation ([Gargron](https://github.com/tootsuite/mastodon/pull/10533))
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove zopfli compression to speed up Webpack from 6min to 1min ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10288))
|
||||
- Remove stats.json generation to speed up Webpack ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10290))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix public timelines being broken by new toots when they are not mounted in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10131))
|
||||
- Fix quick filter settings not being saved when selecting a different filter in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10296))
|
||||
- Fix remote interaction dialogs being indexed by search engines ([Gargron](https://github.com/tootsuite/mastodon/pull/10240))
|
||||
- Fix maxed-out invites not showing up as expired in UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10274))
|
||||
- Fix scrollbar styles on compose textarea ([Gargron](https://github.com/tootsuite/mastodon/pull/10292))
|
||||
- Fix timeline merge workers being queued for remote users ([Gargron](https://github.com/tootsuite/mastodon/pull/10355))
|
||||
- Fix alternative relay support regression ([Gargron](https://github.com/tootsuite/mastodon/pull/10398))
|
||||
- Fix trying to fetch keys of unknown accounts on a self-delete from them ([ThibG](https://github.com/tootsuite/mastodon/pull/10326))
|
||||
- Fix CAS `:service_validate_url` option ([enewhuis](https://github.com/tootsuite/mastodon/pull/10328))
|
||||
- Fix race conditions when creating backups ([ThibG](https://github.com/tootsuite/mastodon/pull/10234))
|
||||
- Fix whitespace not being stripped out of username before validation ([aurelien-reeves](https://github.com/tootsuite/mastodon/pull/10239))
|
||||
- Fix n+1 query when deleting status ([Gargron](https://github.com/tootsuite/mastodon/pull/10247))
|
||||
- Fix exiting follows not being rejected when suspending a remote account ([ThibG](https://github.com/tootsuite/mastodon/pull/10230))
|
||||
- Fix the underlying button element in a disabled icon button not being disabled ([ThibG](https://github.com/tootsuite/mastodon/pull/10194))
|
||||
- Fix race condition when streaming out deleted statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10280))
|
||||
- Fix performance of admin federation UI by caching account counts ([Gargron](https://github.com/tootsuite/mastodon/pull/10374))
|
||||
- Fix JS error on pages that don't define a CSRF token ([hinaloe](https://github.com/tootsuite/mastodon/pull/10383))
|
||||
- Fix `tootctl accounts cull` sometimes removing accounts that are temporarily unreachable ([BenLubar](https://github.com/tootsuite/mastodon/pull/10460))
|
||||
|
||||
## [2.7.4] - 2019-03-05
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ Bug reports and feature suggestions can be submitted to [GitHub Issues](https://
|
|||
|
||||
## Translations
|
||||
|
||||
You can submit translations via [Weblate](https://weblate.joinmastodon.org/). They are periodically merged into the codebase.
|
||||
|
||||
[](https://weblate.joinmastodon.org/)
|
||||
You can submit translations via pull request.
|
||||
|
||||
## Pull requests
|
||||
|
||||
|
|
|
|||
10
Dockerfile
10
Dockerfile
|
|
@ -7,7 +7,6 @@ SHELL ["bash", "-c"]
|
|||
ENV NODE_VER="8.15.0"
|
||||
RUN echo "Etc/UTC" > /etc/localtime && \
|
||||
apt update && \
|
||||
apt -y dist-upgrade && \
|
||||
apt -y install wget make gcc g++ python && \
|
||||
cd ~ && \
|
||||
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \
|
||||
|
|
@ -80,13 +79,12 @@ ARG GID=991
|
|||
RUN apt update && \
|
||||
echo "Etc/UTC" > /etc/localtime && \
|
||||
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
|
||||
apt -y dist-upgrade && \
|
||||
apt install -y whois wget && \
|
||||
addgroup --gid $GID mastodon && \
|
||||
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
|
||||
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
|
||||
|
||||
# Install masto runtime deps
|
||||
# Install mastodon runtime deps
|
||||
RUN apt -y --no-install-recommends install \
|
||||
libssl1.1 libpq5 imagemagick ffmpeg \
|
||||
libicu60 libprotobuf10 libidn11 libyaml-0-2 \
|
||||
|
|
@ -95,7 +93,7 @@ RUN apt -y --no-install-recommends install \
|
|||
ln -s /opt/mastodon /mastodon && \
|
||||
gem install bundler && \
|
||||
rm -rf /var/cache && \
|
||||
rm -rf /var/lib/apt
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add tini
|
||||
ENV TINI_VERSION="0.18.0"
|
||||
|
|
@ -104,11 +102,11 @@ ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tin
|
|||
RUN echo "$TINI_SUM tini" | sha256sum -c -
|
||||
RUN chmod +x /tini
|
||||
|
||||
# Copy over masto source, and dependencies from building, and set permissions
|
||||
# Copy over mastodon source, and dependencies from building, and set permissions
|
||||
COPY --chown=mastodon:mastodon . /opt/mastodon
|
||||
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
|
||||
|
||||
# Run masto services in prod mode
|
||||
# Run mastodon services in prod mode
|
||||
ENV RAILS_ENV="production"
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
|
|
|
|||
29
Gemfile
29
Gemfile
|
|
@ -15,12 +15,13 @@ gem 'makara', '~> 0.4'
|
|||
gem 'pghero', '~> 2.2'
|
||||
gem 'dotenv-rails', '~> 2.7'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.36', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.41', require: false
|
||||
gem 'fog-core', '<= 2.1.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
gem 'paperclip-av-transcoder', '~> 0.6'
|
||||
gem 'streamio-ffmpeg', '~> 3.0'
|
||||
gem 'blurhash', '~> 0.1'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.10'
|
||||
gem 'addressable', '~> 2.6'
|
||||
|
|
@ -29,7 +30,7 @@ gem 'browser'
|
|||
gem 'charlock_holmes', '~> 0.7.6'
|
||||
gem 'iso-639'
|
||||
gem 'chewy', '~> 5.0'
|
||||
gem 'cld3', '~> 3.2.3'
|
||||
gem 'cld3', '~> 3.2.4'
|
||||
gem 'devise', '~> 4.6'
|
||||
gem 'devise-two-factor', '~> 3.0'
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ gem 'omniauth-cas', '~> 1.1'
|
|||
gem 'omniauth-saml', '~> 1.10'
|
||||
gem 'omniauth', '~> 1.9'
|
||||
|
||||
gem 'doorkeeper', '~> 5.0'
|
||||
gem 'doorkeeper', '~> 5.1'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'goldfinger', '~> 2.1'
|
||||
|
|
@ -52,7 +53,7 @@ gem 'htmlentities', '~> 4.3'
|
|||
gem 'http', '~> 3.3'
|
||||
gem 'http_accept_language', '~> 2.1'
|
||||
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2'
|
||||
gem 'httplog', '~> 1.2'
|
||||
gem 'httplog', '~> 1.3'
|
||||
gem 'idn-ruby', require: 'idn'
|
||||
gem 'kaminari', '~> 1.1'
|
||||
gem 'link_header', '~> 0.0'
|
||||
|
|
@ -65,7 +66,7 @@ gem 'ox', '~> 2.10'
|
|||
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
|
||||
gem 'pundit', '~> 2.0'
|
||||
gem 'premailer-rails'
|
||||
gem 'rack-attack', '~> 5.4'
|
||||
gem 'rack-attack', '~> 6.0'
|
||||
gem 'rack-cors', '~> 1.0', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 5.1'
|
||||
gem 'rails-settings-cached', '~> 0.6'
|
||||
|
|
@ -81,9 +82,9 @@ gem 'simple-navigation', '~> 4.0'
|
|||
gem 'simple_form', '~> 4.1'
|
||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||
gem 'stoplight', '~> 2.1.3'
|
||||
gem 'strong_migrations', '~> 0.3'
|
||||
gem 'strong_migrations', '~> 0.4'
|
||||
gem 'tty-command', '~> 0.8', require: false
|
||||
gem 'tty-prompt', '~> 0.18', require: false
|
||||
gem 'tty-prompt', '~> 0.19', require: false
|
||||
gem 'twitter-text', '~> 1.14'
|
||||
gem 'tzinfo-data', '~> 1.2019'
|
||||
gem 'webpacker', '~> 4.0'
|
||||
|
|
@ -95,7 +96,7 @@ gem 'rdf-normalize', '~> 0.3'
|
|||
|
||||
group :development, :test do
|
||||
gem 'fabrication', '~> 2.20'
|
||||
gem 'fuubar', '~> 2.3'
|
||||
gem 'fuubar', '~> 2.4'
|
||||
gem 'i18n-tasks', '~> 0.9', require: false
|
||||
gem 'pry-byebug', '~> 3.7'
|
||||
gem 'pry-rails', '~> 0.3'
|
||||
|
|
@ -107,7 +108,7 @@ group :production, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.15'
|
||||
gem 'capybara', '~> 3.22'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 1.9'
|
||||
gem 'microformats', '~> 4.1'
|
||||
|
|
@ -115,7 +116,7 @@ group :test do
|
|||
gem 'rspec-sidekiq', '~> 3.0'
|
||||
gem 'simplecov', '~> 0.16', require: false
|
||||
gem 'webmock', '~> 3.5'
|
||||
gem 'parallel_tests', '~> 2.28'
|
||||
gem 'parallel_tests', '~> 2.29'
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
|
@ -123,14 +124,14 @@ group :development do
|
|||
gem 'annotate', '~> 2.7'
|
||||
gem 'better_errors', '~> 2.5'
|
||||
gem 'binding_of_caller', '~> 0.7'
|
||||
gem 'bullet', '~> 5.9'
|
||||
gem 'bullet', '~> 6.0'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.3'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 0.66', require: false
|
||||
gem 'rubocop', '~> 0.71', require: false
|
||||
gem 'rubocop-rails', '~> 2.0', require: false
|
||||
gem 'brakeman', '~> 4.5', require: false
|
||||
gem 'bundler-audit', '~> 0.6', require: false
|
||||
gem 'scss_lint', '~> 0.57', require: false
|
||||
|
||||
gem 'capistrano', '~> 3.11'
|
||||
gem 'capistrano-rails', '~> 1.4'
|
||||
|
|
@ -142,7 +143,7 @@ group :development do
|
|||
end
|
||||
|
||||
group :production do
|
||||
gem 'lograge', '~> 0.10'
|
||||
gem 'lograge', '~> 0.11'
|
||||
gem 'redis-rails', '~> 5.0'
|
||||
end
|
||||
|
||||
|
|
|
|||
172
Gemfile.lock
172
Gemfile.lock
|
|
@ -66,8 +66,8 @@ GEM
|
|||
public_suffix (>= 2.0.2, < 4.0)
|
||||
airbrussh (1.3.0)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
annotate (2.7.4)
|
||||
activerecord (>= 3.2, < 6.0)
|
||||
annotate (2.7.5)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 13.0)
|
||||
arel (9.0.0)
|
||||
ast (2.4.0)
|
||||
|
|
@ -75,20 +75,20 @@ GEM
|
|||
encryptor (~> 3.0.0)
|
||||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-eventstream (1.0.2)
|
||||
aws-partitions (1.147.0)
|
||||
aws-sdk-core (3.48.3)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.169.0)
|
||||
aws-sdk-core (3.54.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-partitions (~> 1.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.16.0)
|
||||
aws-sdk-core (~> 3, >= 3.48.2)
|
||||
aws-sdk-kms (1.21.0)
|
||||
aws-sdk-core (~> 3, >= 3.53.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.36.0)
|
||||
aws-sdk-core (~> 3, >= 3.48.2)
|
||||
aws-sdk-s3 (1.41.0)
|
||||
aws-sdk-core (~> 3, >= 3.53.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
bcrypt (3.1.12)
|
||||
|
|
@ -99,12 +99,14 @@ GEM
|
|||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.4.2)
|
||||
blurhash (0.1.3)
|
||||
ffi (~> 1.10.0)
|
||||
bootsnap (1.4.4)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.5.0)
|
||||
brakeman (4.5.1)
|
||||
browser (2.5.3)
|
||||
builder (3.2.3)
|
||||
bullet (5.9.0)
|
||||
bullet (6.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundler-audit (0.6.1)
|
||||
|
|
@ -127,13 +129,13 @@ GEM
|
|||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (3.15.0)
|
||||
capybara (3.22.0)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (~> 1.2)
|
||||
regexp_parser (~> 1.5)
|
||||
xpath (~> 3.2)
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
|
|
@ -143,8 +145,8 @@ GEM
|
|||
elasticsearch (>= 2.0.0)
|
||||
elasticsearch-dsl
|
||||
chunky_png (1.3.10)
|
||||
cld3 (3.2.3)
|
||||
ffi (>= 1.1.0, < 1.10.0)
|
||||
cld3 (3.2.4)
|
||||
ffi (>= 1.1.0, < 1.11.0)
|
||||
climate_control (0.2.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
|
|
@ -184,8 +186,8 @@ GEM
|
|||
docile (1.3.0)
|
||||
domain_name (0.5.20180417)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.0.2)
|
||||
railties (>= 4.2)
|
||||
doorkeeper (5.1.0)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.2)
|
||||
dotenv-rails (2.7.2)
|
||||
dotenv (= 2.7.2)
|
||||
|
|
@ -205,14 +207,14 @@ GEM
|
|||
et-orbi (1.1.6)
|
||||
tzinfo
|
||||
excon (0.62.0)
|
||||
fabrication (2.20.1)
|
||||
fabrication (2.20.2)
|
||||
faker (1.9.3)
|
||||
i18n (>= 0.7)
|
||||
faraday (0.15.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
fastimage (2.1.5)
|
||||
ffi (1.9.25)
|
||||
ffi (1.10.0)
|
||||
fog-core (2.1.0)
|
||||
builder
|
||||
excon (~> 0.58)
|
||||
|
|
@ -229,7 +231,7 @@ GEM
|
|||
fugit (1.1.6)
|
||||
et-orbi (~> 1.1, >= 1.1.6)
|
||||
raabro (~> 1.1)
|
||||
fuubar (2.3.2)
|
||||
fuubar (2.4.0)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
get_process_mem (0.2.3)
|
||||
|
|
@ -240,11 +242,11 @@ GEM
|
|||
http (~> 3.0)
|
||||
nokogiri (~> 1.8)
|
||||
oj (~> 3.0)
|
||||
hamlit (2.9.2)
|
||||
hamlit (2.9.3)
|
||||
temple (>= 0.8.0)
|
||||
thor
|
||||
tilt
|
||||
hamlit-rails (0.2.2)
|
||||
hamlit-rails (0.2.3)
|
||||
actionpack (>= 4.0.1)
|
||||
activesupport (>= 4.0.1)
|
||||
hamlit (>= 1.2.0)
|
||||
|
|
@ -254,7 +256,7 @@ GEM
|
|||
hashdiff (0.3.7)
|
||||
hashie (3.6.0)
|
||||
heapy (0.1.4)
|
||||
highline (2.0.0)
|
||||
highline (2.0.1)
|
||||
hiredis (0.6.3)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
|
|
@ -267,12 +269,12 @@ GEM
|
|||
domain_name (~> 0.5)
|
||||
http-form_data (2.1.1)
|
||||
http_accept_language (2.1.1)
|
||||
httplog (1.2.2)
|
||||
httplog (1.3.0)
|
||||
rack (>= 1.0)
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.6.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (0.9.28)
|
||||
i18n-tasks (0.9.29)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
erubi
|
||||
|
|
@ -318,7 +320,7 @@ GEM
|
|||
letter_opener (~> 1.0)
|
||||
railties (>= 3.2)
|
||||
link_header (0.0.8)
|
||||
lograge (0.10.0)
|
||||
lograge (0.11.1)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
|
|
@ -346,16 +348,16 @@ GEM
|
|||
mini_mime (1.0.1)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
msgpack (1.2.9)
|
||||
msgpack (1.2.10)
|
||||
multi_json (1.13.1)
|
||||
multipart-post (2.0.0)
|
||||
necromancer (0.4.0)
|
||||
necromancer (0.5.0)
|
||||
net-ldap (0.16.1)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (5.0.2)
|
||||
nio4r (2.3.1)
|
||||
nokogiri (1.10.2)
|
||||
nokogiri (1.10.3)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (2.0.0)
|
||||
nokogiri (~> 1.8, >= 1.8.4)
|
||||
|
|
@ -364,7 +366,7 @@ GEM
|
|||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.7.11)
|
||||
oj (3.7.12)
|
||||
omniauth (1.9.0)
|
||||
hashie (>= 3.4.6, < 3.7.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
|
|
@ -380,7 +382,7 @@ GEM
|
|||
addressable (~> 2.5)
|
||||
http (~> 3.0)
|
||||
nokogiri (~> 1.8)
|
||||
ox (2.10.0)
|
||||
ox (2.10.1)
|
||||
paperclip (6.0.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
|
|
@ -390,10 +392,10 @@ GEM
|
|||
paperclip-av-transcoder (0.6.4)
|
||||
av (~> 0.9.0)
|
||||
paperclip (>= 2.5.2)
|
||||
parallel (1.14.0)
|
||||
parallel_tests (2.28.0)
|
||||
parallel (1.17.0)
|
||||
parallel_tests (2.29.0)
|
||||
parallel
|
||||
parser (2.6.0.0)
|
||||
parser (2.6.3.0)
|
||||
ast (~> 2.4.0)
|
||||
pastel (0.7.2)
|
||||
equatable (~> 0.5.0)
|
||||
|
|
@ -418,14 +420,13 @@ GEM
|
|||
pry (~> 0.10)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
psych (3.1.0)
|
||||
public_suffix (3.0.3)
|
||||
public_suffix (3.1.0)
|
||||
puma (3.12.1)
|
||||
pundit (2.0.1)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.1.6)
|
||||
rack (2.0.6)
|
||||
rack-attack (5.4.2)
|
||||
rack (2.0.7)
|
||||
rack-attack (6.0.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.0.3)
|
||||
rack-protection (2.0.5)
|
||||
|
|
@ -469,15 +470,12 @@ GEM
|
|||
thor (>= 0.19.0, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (12.3.2)
|
||||
rb-fsevent (0.10.3)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
rdf (3.0.9)
|
||||
hamster (~> 3.0)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.3.3)
|
||||
rdf (>= 2.2, < 4.0)
|
||||
redis (4.1.0)
|
||||
redis (4.1.2)
|
||||
redis-actionpack (5.0.2)
|
||||
actionpack (>= 4.0, < 6)
|
||||
redis-rack (>= 1, < 3)
|
||||
|
|
@ -496,7 +494,7 @@ GEM
|
|||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.5.0)
|
||||
redis (>= 2.2, < 5)
|
||||
regexp_parser (1.3.0)
|
||||
regexp_parser (1.5.1)
|
||||
request_store (1.4.1)
|
||||
rack (>= 1.4)
|
||||
responders (2.4.1)
|
||||
|
|
@ -526,15 +524,17 @@ GEM
|
|||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.8.0)
|
||||
rubocop (0.66.0)
|
||||
rubocop (0.71.0)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.5, != 2.5.1.1)
|
||||
psych (>= 3.1.0)
|
||||
parser (>= 2.6)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.6)
|
||||
ruby-progressbar (1.10.0)
|
||||
unicode-display_width (>= 1.4.0, < 1.7)
|
||||
rubocop-rails (2.0.0)
|
||||
rack (>= 2.0)
|
||||
rubocop (>= 0.70.0)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-saml (1.9.0)
|
||||
nokogiri (>= 1.5.10)
|
||||
rufus-scheduler (3.5.2)
|
||||
|
|
@ -544,15 +544,7 @@ GEM
|
|||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.8.0)
|
||||
nokogumbo (~> 2.0)
|
||||
sass (3.6.0)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
scss_lint (0.57.1)
|
||||
rake (>= 0.9, < 13)
|
||||
sass (~> 3.5, >= 3.5.5)
|
||||
sidekiq (5.2.5)
|
||||
sidekiq (5.2.7)
|
||||
connection_pool (~> 2.2, >= 2.2.2)
|
||||
rack (>= 1.5.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
|
|
@ -564,7 +556,7 @@ GEM
|
|||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 3)
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (6.0.12)
|
||||
sidekiq-unique-jobs (6.0.13)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 4.0, < 7.0)
|
||||
thor (~> 0)
|
||||
|
|
@ -593,9 +585,9 @@ GEM
|
|||
stoplight (2.1.3)
|
||||
streamio-ffmpeg (3.0.2)
|
||||
multi_json (~> 1.8)
|
||||
strong_migrations (0.3.1)
|
||||
activerecord (>= 3.2.0)
|
||||
temple (0.8.0)
|
||||
strong_migrations (0.4.0)
|
||||
activerecord (>= 5)
|
||||
temple (0.8.1)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
terrapin (0.6.0)
|
||||
|
|
@ -603,22 +595,19 @@ GEM
|
|||
thor (0.20.3)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.9)
|
||||
timers (4.2.0)
|
||||
tty-color (0.4.3)
|
||||
tty-command (0.8.2)
|
||||
pastel (~> 0.7.0)
|
||||
tty-cursor (0.6.0)
|
||||
tty-prompt (0.18.1)
|
||||
necromancer (~> 0.4.0)
|
||||
tty-cursor (0.7.0)
|
||||
tty-prompt (0.19.0)
|
||||
necromancer (~> 0.5.0)
|
||||
pastel (~> 0.7.0)
|
||||
timers (~> 4.0)
|
||||
tty-cursor (~> 0.6.0)
|
||||
tty-reader (~> 0.5.0)
|
||||
tty-reader (0.5.0)
|
||||
tty-cursor (~> 0.6.0)
|
||||
tty-screen (~> 0.6.4)
|
||||
tty-reader (~> 0.6.0)
|
||||
tty-reader (0.6.0)
|
||||
tty-cursor (~> 0.7)
|
||||
tty-screen (~> 0.7)
|
||||
wisper (~> 2.0.0)
|
||||
tty-screen (0.6.5)
|
||||
tty-screen (0.7.0)
|
||||
twitter-text (1.14.7)
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (1.2.5)
|
||||
|
|
@ -628,7 +617,7 @@ GEM
|
|||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.5)
|
||||
unicode-display_width (1.5.0)
|
||||
unicode-display_width (1.6.0)
|
||||
uniform_notifier (1.12.1)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
|
|
@ -636,11 +625,11 @@ GEM
|
|||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
webpacker (4.0.2)
|
||||
webpacker (4.0.7)
|
||||
activesupport (>= 4.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
webpush (0.3.7)
|
||||
webpush (0.3.8)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.0)
|
||||
|
|
@ -658,29 +647,30 @@ DEPENDENCIES
|
|||
active_record_query_trace (~> 1.6)
|
||||
addressable (~> 2.6)
|
||||
annotate (~> 2.7)
|
||||
aws-sdk-s3 (~> 1.36)
|
||||
aws-sdk-s3 (~> 1.41)
|
||||
better_errors (~> 2.5)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.4)
|
||||
brakeman (~> 4.5)
|
||||
browser
|
||||
bullet (~> 5.9)
|
||||
bullet (~> 6.0)
|
||||
bundler-audit (~> 0.6)
|
||||
capistrano (~> 3.11)
|
||||
capistrano-rails (~> 1.4)
|
||||
capistrano-rbenv (~> 2.1)
|
||||
capistrano-yarn (~> 2.0)
|
||||
capybara (~> 3.15)
|
||||
capybara (~> 3.22)
|
||||
charlock_holmes (~> 0.7.6)
|
||||
chewy (~> 5.0)
|
||||
cld3 (~> 3.2.3)
|
||||
cld3 (~> 3.2.4)
|
||||
climate_control (~> 0.2)
|
||||
concurrent-ruby
|
||||
derailed_benchmarks
|
||||
devise (~> 4.6)
|
||||
devise-two-factor (~> 3.0)
|
||||
devise_pam_authenticatable2 (~> 9.2)
|
||||
doorkeeper (~> 5.0)
|
||||
doorkeeper (~> 5.1)
|
||||
dotenv-rails (~> 2.7)
|
||||
fabrication (~> 2.20)
|
||||
faker (~> 1.9)
|
||||
|
|
@ -688,7 +678,7 @@ DEPENDENCIES
|
|||
fastimage
|
||||
fog-core (<= 2.1.0)
|
||||
fog-openstack (~> 0.3)
|
||||
fuubar (~> 2.3)
|
||||
fuubar (~> 2.4)
|
||||
goldfinger (~> 2.1)
|
||||
hamlit-rails (~> 0.2)
|
||||
hiredis (~> 0.6)
|
||||
|
|
@ -696,7 +686,7 @@ DEPENDENCIES
|
|||
http (~> 3.3)
|
||||
http_accept_language (~> 2.1)
|
||||
http_parser.rb (~> 0.6)!
|
||||
httplog (~> 1.2)
|
||||
httplog (~> 1.3)
|
||||
i18n-tasks (~> 0.9)
|
||||
idn-ruby
|
||||
iso-639
|
||||
|
|
@ -706,7 +696,7 @@ DEPENDENCIES
|
|||
letter_opener (~> 1.7)
|
||||
letter_opener_web (~> 1.3)
|
||||
link_header (~> 0.0)
|
||||
lograge (~> 0.10)
|
||||
lograge (~> 0.11)
|
||||
makara (~> 0.4)
|
||||
mario-redis-lock (~> 1.2)
|
||||
memory_profiler
|
||||
|
|
@ -723,7 +713,7 @@ DEPENDENCIES
|
|||
ox (~> 2.10)
|
||||
paperclip (~> 6.0)
|
||||
paperclip-av-transcoder (~> 0.6)
|
||||
parallel_tests (~> 2.28)
|
||||
parallel_tests (~> 2.29)
|
||||
pg (~> 1.1)
|
||||
pghero (~> 2.2)
|
||||
pkg-config (~> 1.3)
|
||||
|
|
@ -734,7 +724,7 @@ DEPENDENCIES
|
|||
pry-rails (~> 0.3)
|
||||
puma (~> 3.12)
|
||||
pundit (~> 2.0)
|
||||
rack-attack (~> 5.4)
|
||||
rack-attack (~> 6.0)
|
||||
rack-cors (~> 1.0)
|
||||
rails (~> 5.2.3)
|
||||
rails-controller-testing (~> 1.0)
|
||||
|
|
@ -747,9 +737,9 @@ DEPENDENCIES
|
|||
rqrcode (~> 0.10)
|
||||
rspec-rails (~> 3.8)
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rubocop (~> 0.66)
|
||||
rubocop (~> 0.71)
|
||||
rubocop-rails (~> 2.0)
|
||||
sanitize (~> 5.0)
|
||||
scss_lint (~> 0.57)
|
||||
sidekiq (~> 5.2)
|
||||
sidekiq-bulk (~> 0.2.0)
|
||||
sidekiq-scheduler (~> 3.0)
|
||||
|
|
@ -761,10 +751,10 @@ DEPENDENCIES
|
|||
stackprof
|
||||
stoplight (~> 2.1.3)
|
||||
streamio-ffmpeg (~> 3.0)
|
||||
strong_migrations (~> 0.3)
|
||||
strong_migrations (~> 0.4)
|
||||
thor (~> 0.20)
|
||||
tty-command (~> 0.8)
|
||||
tty-prompt (~> 0.18)
|
||||
tty-prompt (~> 0.19)
|
||||
twitter-text (~> 1.14)
|
||||
tzinfo-data (~> 1.2019)
|
||||
webmock (~> 3.5)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ class AboutController < ApplicationController
|
|||
private
|
||||
|
||||
def new_user
|
||||
User.new.tap(&:build_account)
|
||||
User.new.tap do |user|
|
||||
user.build_account
|
||||
user.build_invite_request
|
||||
end
|
||||
end
|
||||
|
||||
helper_method :new_user
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
format.json do
|
||||
mark_cacheable!
|
||||
|
||||
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
|
||||
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,13 +6,17 @@ class ActivityPub::CollectionsController < Api::BaseController
|
|||
before_action :set_account
|
||||
before_action :set_size
|
||||
before_action :set_statuses
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
render json: collection_presenter,
|
||||
serializer: ActivityPub::CollectionSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json',
|
||||
skip_activities: true
|
||||
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
collection_presenter,
|
||||
serializer: ActivityPub::CollectionSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
skip_activities: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ class ActivityPub::InboxesController < Api::BaseController
|
|||
end
|
||||
|
||||
def body
|
||||
@body ||= request.body.read.force_encoding('UTF-8')
|
||||
return @body if defined?(@body)
|
||||
@body = request.body.read.force_encoding('UTF-8')
|
||||
request.body.rewind if request.body.respond_to?(:rewind)
|
||||
@body
|
||||
end
|
||||
|
||||
def upgrade_account
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ class ActivityPub::OutboxesController < Api::BaseController
|
|||
|
||||
before_action :set_account
|
||||
before_action :set_statuses
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
expires_in 1.minute, public: true unless page_requested?
|
||||
|
||||
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -48,13 +48,13 @@ module Admin
|
|||
def approve
|
||||
authorize @account.user, :approve?
|
||||
@account.user.approve!
|
||||
redirect_to admin_accounts_path(pending: '1')
|
||||
redirect_to admin_pending_accounts_path
|
||||
end
|
||||
|
||||
def reject
|
||||
authorize @account.user, :reject?
|
||||
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
|
||||
redirect_to admin_accounts_path(pending: '1')
|
||||
redirect_to admin_pending_accounts_path
|
||||
end
|
||||
|
||||
def unsilence
|
||||
|
|
|
|||
|
|
@ -13,13 +13,25 @@ module Admin
|
|||
authorize :domain_block, :create?
|
||||
|
||||
@domain_block = DomainBlock.new(resource_params)
|
||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil
|
||||
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id)
|
||||
log_action :create, @domain_block
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
else
|
||||
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
||||
@domain_block.save
|
||||
flash[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
|
||||
@domain_block.errors[:domain].clear
|
||||
render :new
|
||||
else
|
||||
if existing_domain_block.present?
|
||||
@domain_block = existing_domain_block
|
||||
@domain_block.update(resource_params)
|
||||
end
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id)
|
||||
log_action :create, @domain_block
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -29,7 +41,7 @@ module Admin
|
|||
|
||||
def destroy
|
||||
authorize @domain_block, :destroy?
|
||||
UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
|
||||
UnblockDomainService.new.call(@domain_block)
|
||||
log_action :destroy, @domain_block
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg')
|
||||
end
|
||||
|
|
@ -41,11 +53,7 @@ module Admin
|
|||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :retroactive)
|
||||
end
|
||||
|
||||
def retroactive_unblock?
|
||||
ActiveRecord::Type.lookup(:boolean).cast(resource_params[:retroactive])
|
||||
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
52
app/controllers/admin/pending_accounts_controller.rb
Normal file
52
app/controllers/admin/pending_accounts_controller.rb
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class PendingAccountsController < BaseController
|
||||
before_action :set_accounts, only: :index
|
||||
|
||||
def index
|
||||
@form = Form::AccountBatch.new
|
||||
end
|
||||
|
||||
def batch
|
||||
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
ensure
|
||||
redirect_to admin_pending_accounts_path(current_params)
|
||||
end
|
||||
|
||||
def approve_all
|
||||
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save
|
||||
redirect_to admin_pending_accounts_path(current_params)
|
||||
end
|
||||
|
||||
def reject_all
|
||||
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save
|
||||
redirect_to admin_pending_accounts_path(current_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_accounts
|
||||
@accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page])
|
||||
end
|
||||
|
||||
def form_account_batch_params
|
||||
params.require(:form_account_batch).permit(:action, account_ids: [])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:approve]
|
||||
'approve'
|
||||
elsif params[:reject]
|
||||
'reject'
|
||||
end
|
||||
end
|
||||
|
||||
def current_params
|
||||
params.slice(:page).permit(:page)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -9,6 +9,8 @@ class Api::BaseController < ApplicationController
|
|||
skip_before_action :store_current_location
|
||||
skip_before_action :check_user_permissions
|
||||
|
||||
before_action :set_cache_headers
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||
|
|
@ -88,4 +90,8 @@ class Api::BaseController < ApplicationController
|
|||
def authorize_if_got_token!(*scopes)
|
||||
doorkeeper_authorize!(*scopes) if doorkeeper_token
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,11 +19,15 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def load_accounts
|
||||
return [] if @account.user_hides_network? && current_account.id != @account.id
|
||||
return [] if hide_results?
|
||||
|
||||
default_accounts.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
def hide_results?
|
||||
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:active_relationships, :account_stat).references(:active_relationships)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,11 +19,15 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def load_accounts
|
||||
return [] if @account.user_hides_network? && current_account.id != @account.id
|
||||
return [] if hide_results?
|
||||
|
||||
default_accounts.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
def hide_results?
|
||||
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:passive_relationships, :account_stat).references(:passive_relationships)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@proofs = @account.identity_proofs.active
|
||||
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
class Api::V1::CustomEmojisController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
skip_before_action :set_cache_headers
|
||||
|
||||
def index
|
||||
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
|
||||
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Api::V1::Instances::ActivityController < Api::BaseController
|
||||
before_action :require_enabled_api!
|
||||
skip_before_action :set_cache_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Api::V1::Instances::PeersController < Api::BaseController
|
||||
before_action :require_enabled_api!
|
||||
skip_before_action :set_cache_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Api::V1::InstancesController < Api::BaseController
|
||||
respond_to :json
|
||||
skip_before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
render_cached_json('api:v1:instances', expires_in: 5.minutes) do
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
end
|
||||
|
||||
def browserable_account_notifications
|
||||
current_account.notifications.browserable(exclude_types)
|
||||
current_account.notifications.browserable(exclude_types, from_account)
|
||||
end
|
||||
|
||||
def target_statuses_from_notifications
|
||||
|
|
@ -81,6 +81,10 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
val
|
||||
end
|
||||
|
||||
def from_account
|
||||
params[:account_id]
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,13 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::PollsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show
|
||||
before_action :set_poll
|
||||
before_action :refresh_poll
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
@poll = Poll.attached.find(params[:id])
|
||||
ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale?
|
||||
render json: @poll, serializer: REST::PollSerializer, include_results: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_poll
|
||||
@poll = Poll.attached.find(params[:id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
def refresh_poll
|
||||
ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
|
||||
def data_params
|
||||
return {} if params[:data].blank?
|
||||
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
|
||||
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
|
||||
RemovalWorker.perform_async(@status.id)
|
||||
|
||||
render_empty
|
||||
render json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
favourite: alerts_enabled,
|
||||
reblog: alerts_enabled,
|
||||
mention: alerts_enabled,
|
||||
poll: alerts_enabled,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +58,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
end
|
||||
|
||||
def data_params
|
||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
|
||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -152,11 +152,6 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
def mark_cacheable!
|
||||
skip_session!
|
||||
expires_in 0, public: true
|
||||
end
|
||||
|
||||
def skip_session!
|
||||
request.session_options[:skip] = true
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
before_action :set_instance_presenter, only: [:new, :create, :update]
|
||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||
|
||||
def new
|
||||
super(&:build_invite_request)
|
||||
end
|
||||
|
||||
def destroy
|
||||
not_found
|
||||
end
|
||||
|
|
@ -24,17 +28,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
def build_resource(hash = nil)
|
||||
super(hash)
|
||||
|
||||
resource.locale = I18n.locale
|
||||
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
||||
resource.agreement = true
|
||||
resource.locale = I18n.locale
|
||||
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
||||
resource.agreement = true
|
||||
resource.current_sign_in_ip = request.remote_ip
|
||||
|
||||
resource.current_sign_in_ip = request.remote_ip if resource.current_sign_in_ip.nil?
|
||||
resource.build_account if resource.account.nil?
|
||||
end
|
||||
|
||||
def configure_sign_up_params
|
||||
devise_parameter_sanitizer.permit(:sign_up) do |u|
|
||||
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code)
|
||||
u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -87,7 +91,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
|
||||
def set_invite
|
||||
@invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
|
||||
invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
|
||||
@invite = invite&.valid_for_use? ? invite : nil
|
||||
end
|
||||
|
||||
def determine_layout
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ module AccountControllerConcern
|
|||
|
||||
def check_account_suspension
|
||||
if @account.suspended?
|
||||
skip_session!
|
||||
expires_in(3.minutes, public: true)
|
||||
gone
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,13 +43,7 @@ module SignatureVerification
|
|||
return
|
||||
end
|
||||
|
||||
account_stoplight = Stoplight("source:#{request.ip}") { account_from_key_id(signature_params['keyId']) }
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
|
||||
|
||||
account = account_stoplight.run
|
||||
account = account_from_key_id(signature_params['keyId'])
|
||||
|
||||
if account.nil?
|
||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
||||
|
|
@ -62,13 +56,7 @@ module SignatureVerification
|
|||
|
||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||
|
||||
account_stoplight = Stoplight("source:#{request.ip}") { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
|
||||
|
||||
account = account_stoplight.run
|
||||
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
||||
|
||||
if account.nil?
|
||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
||||
|
|
@ -136,14 +124,23 @@ module SignatureVerification
|
|||
|
||||
def account_from_key_id(key_id)
|
||||
if key_id.start_with?('acct:')
|
||||
ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
|
||||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
|
||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
|
||||
account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false)
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) }
|
||||
account
|
||||
end
|
||||
end
|
||||
|
||||
def stoplight_wrap_request(&block)
|
||||
Stoplight("source:#{request.remote_ip}", &block)
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
|
||||
.run
|
||||
end
|
||||
|
||||
def account_refresh_key(account)
|
||||
return if account.local? || !account.activitypub?
|
||||
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ class CustomCssController < ApplicationController
|
|||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
skip_session!
|
||||
render plain: Setting.custom_css || '', content_type: 'text/css'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ class EmojisController < ApplicationController
|
|||
def show
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
|
||||
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
class FollowerAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
|
||||
before_action :set_cache_headers
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
|
@ -17,6 +19,8 @@ class FollowerAccountsController < ApplicationController
|
|||
format.json do
|
||||
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
|
||||
|
||||
expires_in 3.minutes, public: true if params[:page].blank?
|
||||
|
||||
render json: collection_presenter,
|
||||
serializer: ActivityPub::CollectionSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@
|
|||
class FollowingAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
|
||||
before_action :set_cache_headers
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
mark_cacheable! unless user_signed_in?
|
||||
|
||||
next if @account.user_hides_network?
|
||||
|
||||
follows
|
||||
|
|
@ -15,6 +19,8 @@ class FollowingAccountsController < ApplicationController
|
|||
format.json do
|
||||
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
|
||||
|
||||
expires_in 3.minutes, public: true if params[:page].blank?
|
||||
|
||||
render json: collection_presenter,
|
||||
serializer: ActivityPub::CollectionSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class HomeController < ApplicationController
|
|||
push_subscription: current_account.user.web_push_subscription(current_session),
|
||||
current_account: current_account,
|
||||
token: current_session.token,
|
||||
admin: Account.find_local(Setting.site_contact_username),
|
||||
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ class HomeController < ApplicationController
|
|||
if request.path.start_with?('/web')
|
||||
new_user_session_path
|
||||
elsif single_user_mode?
|
||||
short_account_path(Account.local.where(suspended: false).first)
|
||||
short_account_path(Account.local.without_suspended.first)
|
||||
else
|
||||
about_path
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ class Settings::IdentityProofsController < Settings::BaseController
|
|||
provider_username: params[:provider_username]
|
||||
)
|
||||
|
||||
render layout: 'auth'
|
||||
if current_account.username.casecmp(params[:username]).zero?
|
||||
render layout: 'auth'
|
||||
else
|
||||
flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
||||
redirect_to settings_identity_proofs_path
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
@ -26,6 +31,7 @@ class Settings::IdentityProofsController < Settings::BaseController
|
|||
@proof.token = resource_params[:token]
|
||||
|
||||
if @proof.save
|
||||
PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof?
|
||||
redirect_to @proof.on_success_path(params[:user_agent])
|
||||
else
|
||||
flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
||||
|
|
@ -36,10 +42,22 @@ class Settings::IdentityProofsController < Settings::BaseController
|
|||
private
|
||||
|
||||
def check_required_params
|
||||
redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :token].all? { |k| params[k].present? }
|
||||
redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? }
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:account_identity_proof).permit(:provider, :provider_username, :token)
|
||||
end
|
||||
|
||||
def publish_proof?
|
||||
ActiveModel::Type::Boolean.new.cast(post_params[:post_status])
|
||||
end
|
||||
|
||||
def post_params
|
||||
params.require(:account_identity_proof).permit(:post_status, :status_text)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = ''
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::NotificationsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
user_settings.update(user_settings_params.to_h)
|
||||
|
||||
if current_user.save
|
||||
redirect_to settings_notifications_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_settings
|
||||
UserSettingsDecorator.new(current_user)
|
||||
end
|
||||
|
||||
def user_settings_params
|
||||
params.require(:user).permit(
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report),
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::Preferences::AppearanceController < Settings::PreferencesController
|
||||
private
|
||||
|
||||
def after_update_redirect_path
|
||||
settings_preferences_appearance_path
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::Preferences::NotificationsController < Settings::PreferencesController
|
||||
private
|
||||
|
||||
def after_update_redirect_path
|
||||
settings_preferences_notifications_path
|
||||
end
|
||||
end
|
||||
9
app/controllers/settings/preferences/other_controller.rb
Normal file
9
app/controllers/settings/preferences/other_controller.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::Preferences::OtherController < Settings::PreferencesController
|
||||
private
|
||||
|
||||
def after_update_redirect_path
|
||||
settings_preferences_other_path
|
||||
end
|
||||
end
|
||||
|
|
@ -12,7 +12,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
|
||||
if current_user.update(user_params)
|
||||
I18n.locale = current_user.locale
|
||||
redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render :show
|
||||
end
|
||||
|
|
@ -20,6 +20,10 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
|
||||
private
|
||||
|
||||
def after_update_redirect_path
|
||||
settings_preferences_path
|
||||
end
|
||||
|
||||
def user_settings
|
||||
UserSettingsDecorator.new(current_user)
|
||||
end
|
||||
|
|
@ -49,8 +53,9 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_hide_network,
|
||||
:setting_aggregate_reblogs,
|
||||
:setting_show_application,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report),
|
||||
interactions: %i(must_be_follower must_be_following)
|
||||
:setting_advanced_layout,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class SharesController < ApplicationController
|
|||
push_subscription: current_account.user.web_push_subscription(current_session),
|
||||
current_account: current_account,
|
||||
token: current_session.token,
|
||||
admin: Account.find_local(Setting.site_contact_username),
|
||||
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
|
||||
text: text,
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class StatusesController < ApplicationController
|
|||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
mark_cacheable! unless user_signed_in?
|
||||
expires_in 10.seconds, public: true if current_account.nil?
|
||||
|
||||
@body_classes = 'with-modals'
|
||||
|
||||
|
|
@ -38,8 +38,6 @@ class StatusesController < ApplicationController
|
|||
end
|
||||
|
||||
format.json do
|
||||
mark_cacheable! unless @stream_entry.hidden?
|
||||
|
||||
render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
|
|
@ -48,8 +46,6 @@ class StatusesController < ApplicationController
|
|||
end
|
||||
|
||||
def activity
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
|
|
@ -58,7 +54,6 @@ class StatusesController < ApplicationController
|
|||
def embed
|
||||
raise ActiveRecord::RecordNotFound if @status.hidden?
|
||||
|
||||
skip_session!
|
||||
expires_in 180, public: true
|
||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
|
||||
|
|
@ -67,8 +62,6 @@ class StatusesController < ApplicationController
|
|||
end
|
||||
|
||||
def replies
|
||||
skip_session!
|
||||
|
||||
render json: replies_collection_presenter,
|
||||
serializer: ActivityPub::CollectionSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
|
|
|
|||
|
|
@ -15,14 +15,13 @@ class StreamEntriesController < ApplicationController
|
|||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
|
||||
expires_in 5.minutes, public: true unless @stream_entry.hidden?
|
||||
|
||||
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity)
|
||||
end
|
||||
|
||||
format.atom do
|
||||
unless @stream_entry.hidden?
|
||||
skip_session!
|
||||
expires_in 3.minutes, public: true
|
||||
end
|
||||
expires_in 3.minutes, public: true unless @stream_entry.hidden?
|
||||
|
||||
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
|
||||
end
|
||||
|
|
@ -50,7 +49,7 @@ class StreamEntriesController < ApplicationController
|
|||
|
||||
def set_stream_entry
|
||||
@stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id])
|
||||
@type = @stream_entry.activity_type.downcase
|
||||
@type = 'status'
|
||||
|
||||
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
|
||||
authorize @stream_entry.activity, :show? if @stream_entry.hidden?
|
||||
|
|
|
|||
|
|
@ -117,4 +117,9 @@ module ApplicationHelper
|
|||
def storage_host?
|
||||
ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present?
|
||||
end
|
||||
|
||||
def quote_wrap(text, line_width: 80, break_sequence: "\n")
|
||||
text = word_wrap(text, line_width: line_width - 2, break_sequence: break_sequence)
|
||||
text.split("\n").map { |line| '> ' + line }.join("\n")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,24 +16,32 @@ module StreamEntriesHelper
|
|||
if user_signed_in?
|
||||
if account.id == current_user.account_id
|
||||
link_to settings_profile_url, class: 'button logo-button' do
|
||||
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('settings.edit_profile')])
|
||||
safe_join([svg_logo, t('settings.edit_profile')])
|
||||
end
|
||||
elsif current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
|
||||
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')])
|
||||
safe_join([svg_logo, t('accounts.unfollow')])
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: 'button logo-button', data: { method: :post } do
|
||||
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')])
|
||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
||||
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')])
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def svg_logo
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||
end
|
||||
|
||||
def svg_logo_full
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
|
||||
end
|
||||
|
||||
def account_badge(account, all: false)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.6 KiB |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#fff"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"><symbol id="mastodon-svg-logo" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" /></symbol></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
|
@ -34,6 +34,11 @@ export function showAlertForError(error) {
|
|||
if (error.response) {
|
||||
const { data, status, statusText } = error.response;
|
||||
|
||||
if (status === 404 || status === 410) {
|
||||
// Skip these errors as they are reflected in the UI
|
||||
return {};
|
||||
}
|
||||
|
||||
let message = statusText;
|
||||
let title = `${status}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,14 @@ const messages = defineMessages({
|
|||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
});
|
||||
|
||||
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
|
||||
|
||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||
if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
|
||||
routerHistory.push('/statuses/new');
|
||||
}
|
||||
};
|
||||
|
||||
export function changeCompose(text) {
|
||||
return {
|
||||
type: COMPOSE_CHANGE,
|
||||
|
|
@ -77,9 +85,7 @@ export function replyCompose(status, routerHistory) {
|
|||
status: status,
|
||||
});
|
||||
|
||||
if (!getState().getIn(['compose', 'mounted'])) {
|
||||
routerHistory.push('/statuses/new');
|
||||
}
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -102,9 +108,7 @@ export function mentionCompose(account, routerHistory) {
|
|||
account: account,
|
||||
});
|
||||
|
||||
if (!getState().getIn(['compose', 'mounted'])) {
|
||||
routerHistory.push('/statuses/new');
|
||||
}
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -115,9 +119,7 @@ export function directCompose(account, routerHistory) {
|
|||
account: account,
|
||||
});
|
||||
|
||||
if (!getState().getIn(['compose', 'mounted'])) {
|
||||
routerHistory.push('/statuses/new');
|
||||
}
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -203,8 +205,8 @@ export function uploadCompose(files) {
|
|||
return function (dispatch, getState) {
|
||||
const uploadLimit = 4;
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||
const progress = new Array(files.length).fill(0);
|
||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||
|
||||
if (files.length + media.size > uploadLimit) {
|
||||
dispatch(showAlert(undefined, messages.uploadErrorLimit));
|
||||
|
|
@ -224,6 +226,8 @@ export function uploadCompose(files) {
|
|||
resizeImage(f).then(file => {
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
// Account for disparity in size of original image and resized data
|
||||
total += file.size - f.size;
|
||||
|
||||
return api(getState).post('/api/v1/media', data, {
|
||||
onUploadProgress: function({ loaded }){
|
||||
|
|
@ -381,7 +385,7 @@ export function readyComposeSuggestionsAccounts(token, accounts) {
|
|||
};
|
||||
};
|
||||
|
||||
export function selectComposeSuggestion(position, token, suggestion) {
|
||||
export function selectComposeSuggestion(position, token, suggestion, path) {
|
||||
return (dispatch, getState) => {
|
||||
let completion, startPosition;
|
||||
|
||||
|
|
@ -403,6 +407,7 @@ export function selectComposeSuggestion(position, token, suggestion) {
|
|||
position: startPosition,
|
||||
token,
|
||||
completion,
|
||||
path,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
|||
30
app/javascript/mastodon/actions/identity_proofs.js
Normal file
30
app/javascript/mastodon/actions/identity_proofs.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import api from '../api';
|
||||
|
||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST';
|
||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS';
|
||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL';
|
||||
|
||||
export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => {
|
||||
dispatch(fetchAccountIdentityProofsRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`)
|
||||
.then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err)));
|
||||
};
|
||||
|
||||
export const fetchAccountIdentityProofsRequest = id => ({
|
||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({
|
||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
|
||||
accountId,
|
||||
identity_proofs,
|
||||
});
|
||||
|
||||
export const fetchAccountIdentityProofsFail = (accountId, err) => ({
|
||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
|
||||
accountId,
|
||||
err,
|
||||
});
|
||||
|
|
@ -37,6 +37,7 @@ export function submitSearch() {
|
|||
params: {
|
||||
q: value,
|
||||
resolve: true,
|
||||
limit: 5,
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.data.accounts) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { evictStatus } from '../storage/modifier';
|
|||
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
|
||||
import { ensureComposeIsVisible } from './compose';
|
||||
|
||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||
|
|
@ -131,14 +132,15 @@ export function fetchStatusFail(id, error, skipLoading) {
|
|||
};
|
||||
};
|
||||
|
||||
export function redraft(status) {
|
||||
export function redraft(status, raw_text) {
|
||||
return {
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteStatus(id, router, withRedraft = false) {
|
||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||
return (dispatch, getState) => {
|
||||
let status = getState().getIn(['statuses', id]);
|
||||
|
||||
|
|
@ -148,17 +150,14 @@ export function deleteStatus(id, router, withRedraft = false) {
|
|||
|
||||
dispatch(deleteStatusRequest(id));
|
||||
|
||||
api(getState).delete(`/api/v1/statuses/${id}`).then(() => {
|
||||
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
|
||||
evictStatus(id);
|
||||
dispatch(deleteStatusSuccess(id));
|
||||
dispatch(deleteFromTimelines(id));
|
||||
|
||||
if (withRedraft) {
|
||||
dispatch(redraft(status));
|
||||
|
||||
if (!getState().getIn(['compose', 'mounted'])) {
|
||||
router.push('/statuses/new');
|
||||
}
|
||||
dispatch(redraft(status, response.data.text));
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
}
|
||||
}).catch(error => {
|
||||
dispatch(deleteStatusFail(id, error));
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done =
|
|||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
|
|
|
|||
229
app/javascript/mastodon/components/autosuggest_input.js
Normal file
229
app/javascript/mastodon/components/autosuggest_input.js
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import React from 'react';
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isRtl } from '../rtl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import classNames from 'classnames';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
||||
let word;
|
||||
|
||||
let left = str.slice(0, caretPosition).search(/\S+$/);
|
||||
let right = str.slice(caretPosition).search(/\s/);
|
||||
|
||||
if (right < 0) {
|
||||
word = str.slice(left);
|
||||
} else {
|
||||
word = str.slice(left, right + caretPosition);
|
||||
}
|
||||
|
||||
if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
word = word.trim().toLowerCase();
|
||||
|
||||
if (word.length > 0) {
|
||||
return [left + 1, word];
|
||||
} else {
|
||||
return [null, null];
|
||||
}
|
||||
};
|
||||
|
||||
export default class AutosuggestInput extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
autoFocus: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
searchTokens: PropTypes.arrayOf(PropTypes.string),
|
||||
maxLength: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
autoFocus: true,
|
||||
searchTokens: ImmutableList(['@', ':', '#']),
|
||||
};
|
||||
|
||||
state = {
|
||||
suggestionsHidden: true,
|
||||
focused: false,
|
||||
selectedSuggestion: 0,
|
||||
lastToken: null,
|
||||
tokenStart: 0,
|
||||
};
|
||||
|
||||
onChange = (e) => {
|
||||
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens);
|
||||
|
||||
if (token !== null && this.state.lastToken !== token) {
|
||||
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
|
||||
this.props.onSuggestionsFetchRequested(token);
|
||||
} else if (token === null) {
|
||||
this.setState({ lastToken: null });
|
||||
this.props.onSuggestionsClearRequested();
|
||||
}
|
||||
|
||||
this.props.onChange(e);
|
||||
}
|
||||
|
||||
onKeyDown = (e) => {
|
||||
const { suggestions, disabled } = this.props;
|
||||
const { selectedSuggestion, suggestionsHidden } = this.state;
|
||||
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.which === 229 || e.isComposing) {
|
||||
// Ignore key events during text composition
|
||||
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
|
||||
return;
|
||||
}
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
if (suggestions.size === 0 || suggestionsHidden) {
|
||||
document.querySelector('.ui').parentElement.focus();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
this.setState({ suggestionsHidden: true });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
// Select suggestion
|
||||
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (e.defaultPrevented || !this.props.onKeyDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ suggestionsHidden: true, focused: false });
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({ focused: true });
|
||||
}
|
||||
|
||||
onSuggestionClick = (e) => {
|
||||
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
}
|
||||
}
|
||||
|
||||
setInput = (c) => {
|
||||
this.input = c;
|
||||
}
|
||||
|
||||
renderSuggestion = (suggestion, i) => {
|
||||
const { selectedSuggestion } = this.state;
|
||||
let inner, key;
|
||||
|
||||
if (typeof suggestion === 'object') {
|
||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||
key = suggestion.id;
|
||||
} else if (suggestion[0] === '#') {
|
||||
inner = suggestion;
|
||||
key = suggestion;
|
||||
} else {
|
||||
inner = <AutosuggestAccountContainer id={suggestion} />;
|
||||
key = suggestion;
|
||||
}
|
||||
|
||||
return (
|
||||
<div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
const style = { direction: 'ltr' };
|
||||
|
||||
if (isRtl(value)) {
|
||||
style.direction = 'rtl';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='autosuggest-input'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
ref={this.setInput}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
style={style}
|
||||
aria-autocomplete='list'
|
||||
id={id}
|
||||
className={className}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -55,7 +55,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
state = {
|
||||
suggestionsHidden: false,
|
||||
suggestionsHidden: true,
|
||||
focused: false,
|
||||
selectedSuggestion: 0,
|
||||
lastToken: null,
|
||||
tokenStart: 0,
|
||||
|
|
@ -134,7 +135,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ suggestionsHidden: true });
|
||||
this.setState({ suggestionsHidden: true, focused: false });
|
||||
}
|
||||
|
||||
onFocus = (e) => {
|
||||
this.setState({ focused: true });
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(e);
|
||||
}
|
||||
}
|
||||
|
||||
onSuggestionClick = (e) => {
|
||||
|
|
@ -145,7 +153,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
|
||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
}
|
||||
}
|
||||
|
|
@ -184,7 +192,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props;
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
const style = { direction: 'ltr' };
|
||||
|
||||
|
|
@ -192,33 +200,39 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
style.direction = 'rtl';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='autosuggest-textarea'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
return [
|
||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||
<div className='autosuggest-textarea'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
|
||||
<Textarea
|
||||
inputRef={this.setTextarea}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onBlur={this.onBlur}
|
||||
onPaste={this.onPaste}
|
||||
style={style}
|
||||
aria-autocomplete='list'
|
||||
/>
|
||||
</label>
|
||||
<Textarea
|
||||
inputRef={this.setTextarea}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onPaste={this.onPaste}
|
||||
style={style}
|
||||
aria-autocomplete='list'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{children}
|
||||
</div>,
|
||||
|
||||
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
20
app/javascript/mastodon/components/icon_with_badge.js
Normal file
20
app/javascript/mastodon/components/icon_with_badge.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const formatNumber = num => num > 40 ? '40+' : num;
|
||||
|
||||
const IconWithBadge = ({ id, count, className }) => (
|
||||
<i className='icon-with-badge'>
|
||||
<Icon id={id} fixedWidth className={className} />
|
||||
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
|
||||
</i>
|
||||
);
|
||||
|
||||
IconWithBadge.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default IconWithBadge;
|
||||
|
|
@ -7,6 +7,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||
import { isIOS } from '../is_mobile';
|
||||
import classNames from 'classnames';
|
||||
import { autoPlayGif, displayMedia } from '../initial_state';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
|
|
@ -21,6 +22,7 @@ class Item extends React.PureComponent {
|
|||
size: PropTypes.number.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
displayWidth: PropTypes.number,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -29,6 +31,10 @@ class Item extends React.PureComponent {
|
|||
size: 1,
|
||||
};
|
||||
|
||||
state = {
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
handleMouseEnter = (e) => {
|
||||
if (this.hoverToPlay()) {
|
||||
e.target.play();
|
||||
|
|
@ -62,8 +68,40 @@ class Item extends React.PureComponent {
|
|||
e.stopPropagation();
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.attachment.get('blurhash')) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
_decode () {
|
||||
const hash = this.props.attachment.get('blurhash');
|
||||
const pixels = decode(hash, 32, 32);
|
||||
|
||||
if (pixels) {
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, 32, 32);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
}
|
||||
|
||||
handleImageLoad = () => {
|
||||
this.setState({ loaded: true });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { attachment, index, size, standalone, displayWidth } = this.props;
|
||||
const { attachment, index, size, standalone, displayWidth, visible } = this.props;
|
||||
|
||||
let width = 50;
|
||||
let height = 100;
|
||||
|
|
@ -116,12 +154,20 @@ class Item extends React.PureComponent {
|
|||
|
||||
let thumbnail = '';
|
||||
|
||||
if (attachment.get('type') === 'image') {
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else if (attachment.get('type') === 'image') {
|
||||
const previewUrl = attachment.get('preview_url');
|
||||
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
|
||||
|
||||
const originalUrl = attachment.get('url');
|
||||
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
||||
const originalUrl = attachment.get('url');
|
||||
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
||||
|
||||
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
|
||||
|
||||
|
|
@ -147,6 +193,7 @@ class Item extends React.PureComponent {
|
|||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
|
@ -176,7 +223,8 @@ class Item extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
{thumbnail}
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
|
||||
{visible && thumbnail}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -196,6 +244,8 @@ class MediaGallery extends React.PureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
defaultWidth: PropTypes.number,
|
||||
cacheWidth: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
onToggleVisibility: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -203,18 +253,24 @@ class MediaGallery extends React.PureComponent {
|
|||
};
|
||||
|
||||
state = {
|
||||
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
|
||||
visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
||||
width: this.props.defaultWidth,
|
||||
};
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!is(nextProps.media, this.props.media)) {
|
||||
this.setState({ visible: !nextProps.sensitive });
|
||||
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
|
||||
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
|
||||
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
|
||||
this.setState({ visible: nextProps.visible });
|
||||
}
|
||||
}
|
||||
|
||||
handleOpen = () => {
|
||||
this.setState({ visible: !this.state.visible });
|
||||
if (this.props.onToggleVisibility) {
|
||||
this.props.onToggleVisibility();
|
||||
} else {
|
||||
this.setState({ visible: !this.state.visible });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = (index) => {
|
||||
|
|
@ -225,6 +281,7 @@ class MediaGallery extends React.PureComponent {
|
|||
if (node /*&& this.isStandaloneEligible()*/) {
|
||||
// offsetWidth triggers a layout, so only calculate when we need to
|
||||
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
|
||||
|
||||
this.setState({
|
||||
width: node.offsetWidth,
|
||||
});
|
||||
|
|
@ -242,7 +299,7 @@ class MediaGallery extends React.PureComponent {
|
|||
|
||||
const width = this.state.width || defaultWidth;
|
||||
|
||||
let children;
|
||||
let children, spoilerButton;
|
||||
|
||||
const style = {};
|
||||
|
||||
|
|
@ -256,35 +313,28 @@ class MediaGallery extends React.PureComponent {
|
|||
style.height = height;
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
let warning;
|
||||
const size = media.take(4).size;
|
||||
|
||||
if (sensitive) {
|
||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||
} else {
|
||||
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
|
||||
}
|
||||
if (this.isStandaloneEligible()) {
|
||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
||||
} else {
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible} />);
|
||||
}
|
||||
|
||||
children = (
|
||||
<button type='button' className='media-spoiler' onClick={this.handleOpen} style={style} ref={this.handleRef}>
|
||||
<span className='media-spoiler__warning'>{warning}</span>
|
||||
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
if (visible) {
|
||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
||||
} else {
|
||||
spoilerButton = (
|
||||
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
|
||||
<span className='spoiler-button__overlay__label'>{sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}</span>
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
const size = media.take(4).size;
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} />;
|
||||
} else {
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} />);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='media-gallery' style={style} ref={this.handleRef}>
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}>
|
||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
|
||||
{spoilerButton}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -9,41 +9,12 @@ import Motion from 'mastodon/features/ui/util/optional_motion';
|
|||
import spring from 'react-motion/lib/spring';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import emojify from 'mastodon/features/emoji/emoji';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
|
||||
const messages = defineMessages({
|
||||
moments: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
|
||||
seconds: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
|
||||
minutes: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
|
||||
hours: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
|
||||
days: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
});
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 1000 * 60;
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
const DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
const timeRemainingString = (intl, date, now) => {
|
||||
const delta = date.getTime() - now;
|
||||
|
||||
let relativeTime;
|
||||
|
||||
if (delta < 10 * SECOND) {
|
||||
relativeTime = intl.formatMessage(messages.moments);
|
||||
} else if (delta < MINUTE) {
|
||||
relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
|
||||
} else if (delta < HOUR) {
|
||||
relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
|
||||
} else if (delta < DAY) {
|
||||
relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
|
||||
} else {
|
||||
relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
|
||||
}
|
||||
|
||||
return relativeTime;
|
||||
};
|
||||
|
||||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
|
||||
return obj;
|
||||
|
|
@ -146,7 +117,7 @@ class Poll extends ImmutablePureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now());
|
||||
const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
|
||||
const showResults = poll.get('voted') || poll.get('expired');
|
||||
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ const messages = defineMessages({
|
|||
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
|
||||
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
|
||||
days: { id: 'relative_time.days', defaultMessage: '{number}d' },
|
||||
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
|
||||
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
|
||||
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
|
||||
hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
|
||||
days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
|
||||
});
|
||||
|
||||
const dateFormatOptions = {
|
||||
|
|
@ -86,6 +91,26 @@ export const timeAgoString = (intl, date, now, year) => {
|
|||
return relativeTime;
|
||||
};
|
||||
|
||||
const timeRemainingString = (intl, date, now) => {
|
||||
const delta = date.getTime() - now;
|
||||
|
||||
let relativeTime;
|
||||
|
||||
if (delta < 10 * SECOND) {
|
||||
relativeTime = intl.formatMessage(messages.moments_remaining);
|
||||
} else if (delta < MINUTE) {
|
||||
relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
|
||||
} else if (delta < HOUR) {
|
||||
relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) });
|
||||
} else if (delta < DAY) {
|
||||
relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) });
|
||||
} else {
|
||||
relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) });
|
||||
}
|
||||
|
||||
return relativeTime;
|
||||
};
|
||||
|
||||
export default @injectIntl
|
||||
class RelativeTimestamp extends React.Component {
|
||||
|
||||
|
|
@ -93,6 +118,7 @@ class RelativeTimestamp extends React.Component {
|
|||
intl: PropTypes.object.isRequired,
|
||||
timestamp: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
futureDate: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
|
@ -145,10 +171,10 @@ class RelativeTimestamp extends React.Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { timestamp, intl, year } = this.props;
|
||||
const { timestamp, intl, year, futureDate } = this.props;
|
||||
|
||||
const date = new Date(timestamp);
|
||||
const relativeTime = timeAgoString(intl, date, this.state.now, year);
|
||||
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year);
|
||||
|
||||
return (
|
||||
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { MediaGallery, Video } from '../features/ui/util/async-components';
|
|||
import { HotKeys } from 'react-hotkeys';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import PollContainer from 'mastodon/containers/poll_container';
|
||||
import { displayMedia } from '../initial_state';
|
||||
|
||||
// We use the component (and not the container) since we do not want
|
||||
// to use the progress bar to show download progress
|
||||
|
|
@ -39,6 +39,18 @@ export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
|||
return values.join(', ');
|
||||
};
|
||||
|
||||
export const defaultMediaVisibility = (status) => {
|
||||
if (!status) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||
status = status.get('reblog');
|
||||
}
|
||||
|
||||
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
||||
};
|
||||
|
||||
export default @injectIntl
|
||||
class Status extends ImmutablePureComponent {
|
||||
|
||||
|
|
@ -85,6 +97,11 @@ class Status extends ImmutablePureComponent {
|
|||
'hidden',
|
||||
];
|
||||
|
||||
state = {
|
||||
showMedia: defaultMediaVisibility(this.props.status),
|
||||
statusId: undefined,
|
||||
};
|
||||
|
||||
// Track height changes we know about to compensate scrolling
|
||||
componentDidMount () {
|
||||
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||
|
|
@ -98,11 +115,24 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
||||
return {
|
||||
showMedia: defaultMediaVisibility(nextProps.status),
|
||||
statusId: nextProps.status.get('id'),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Compensate height changes
|
||||
componentDidUpdate (prevProps, prevState, snapshot) {
|
||||
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||
|
||||
if (doShowCard && !this.didShowCard) {
|
||||
this.didShowCard = true;
|
||||
|
||||
if (snapshot !== null && this.props.updateScrollBottom) {
|
||||
if (this.node && this.node.offsetTop < snapshot.top) {
|
||||
this.props.updateScrollBottom(snapshot.height - snapshot.top);
|
||||
|
|
@ -122,6 +152,10 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleToggleMediaVisibility = () => {
|
||||
this.setState({ showMedia: !this.state.showMedia });
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
|
|
@ -136,6 +170,17 @@ class Status extends ImmutablePureComponent {
|
|||
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
|
||||
}
|
||||
|
||||
handleExpandClick = (e) => {
|
||||
if (e.button === 0) {
|
||||
if (!this.context.router) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status } = this.props;
|
||||
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
const id = e.currentTarget.getAttribute('data-id');
|
||||
|
|
@ -198,6 +243,10 @@ class Status extends ImmutablePureComponent {
|
|||
this.props.onToggleHidden(this._properStatus());
|
||||
}
|
||||
|
||||
handleHotkeyToggleSensitive = () => {
|
||||
this.handleToggleMediaVisibility();
|
||||
}
|
||||
|
||||
_properStatus () {
|
||||
const { status } = this.props;
|
||||
|
||||
|
|
@ -271,10 +320,8 @@ class Status extends ImmutablePureComponent {
|
|||
status = status.get('reblog');
|
||||
}
|
||||
|
||||
if (status.get('poll')) {
|
||||
media = <PollContainer pollId={status.get('poll')} />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (this.props.muted) {
|
||||
media = (
|
||||
<AttachmentList
|
||||
compact
|
||||
|
|
@ -289,6 +336,7 @@ class Status extends ImmutablePureComponent {
|
|||
{Component => (
|
||||
<Component
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={this.props.cachedMediaWidth}
|
||||
|
|
@ -297,6 +345,8 @@ class Status extends ImmutablePureComponent {
|
|||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
|
|
@ -312,6 +362,8 @@ class Status extends ImmutablePureComponent {
|
|||
onOpenMedia={this.props.onOpenMedia}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
|
|
@ -347,14 +399,16 @@ class Status extends ImmutablePureComponent {
|
|||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
toggleHidden: this.handleHotkeyToggleHidden,
|
||||
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||
};
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} ref={this.handleRef}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||
{prepend}
|
||||
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
||||
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
||||
<div className='status__info'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { isRtl } from '../rtl';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import Permalink from './permalink';
|
||||
import classnames from 'classnames';
|
||||
import PollContainer from 'mastodon/containers/poll_container';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
||||
|
|
@ -191,6 +192,8 @@ export default class StatusContent extends React.PureComponent {
|
|||
{mentionsPlaceholder}
|
||||
|
||||
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
|
||||
|
||||
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.onClick) {
|
||||
|
|
@ -212,9 +215,13 @@ export default class StatusContent extends React.PureComponent {
|
|||
output.push(readMoreButton);
|
||||
}
|
||||
|
||||
if (status.get('poll')) {
|
||||
output.push(<PollContainer pollId={status.get('poll')} />);
|
||||
}
|
||||
|
||||
return output;
|
||||
} else {
|
||||
return (
|
||||
const output = [
|
||||
<div
|
||||
tabIndex='0'
|
||||
ref={this.setRef}
|
||||
|
|
@ -222,8 +229,14 @@ export default class StatusContent extends React.PureComponent {
|
|||
style={directionStyle}
|
||||
dangerouslySetInnerHTML={content}
|
||||
lang={status.get('language')}
|
||||
/>
|
||||
);
|
||||
/>,
|
||||
];
|
||||
|
||||
if (status.get('poll')) {
|
||||
output.push(<PollContainer pollId={status.get('poll')} />);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,22 +46,28 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
|
||||
handleMoveUp = (id, featured) => {
|
||||
const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = (id, featured) => {
|
||||
const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
handleLoadOlder = debounce(() => {
|
||||
this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
|
||||
}, 300, { leading: true })
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
if (align_top && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,18 +69,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
},
|
||||
|
||||
onModalReblog (status) {
|
||||
dispatch(reblog(status));
|
||||
},
|
||||
|
||||
onReblog (status, e) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||
}
|
||||
dispatch(reblog(status));
|
||||
}
|
||||
},
|
||||
|
||||
onReblog (status, e) {
|
||||
if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ const messages = defineMessages({
|
|||
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
||||
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
|
||||
direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
|
|
@ -62,6 +60,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
identity_props: ImmutablePropTypes.list,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
|
@ -81,7 +80,7 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { account, intl, domain } = this.props;
|
||||
const { account, intl, domain, identity_proofs } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
|
|
@ -93,15 +92,15 @@ class Header extends ImmutablePureComponent {
|
|||
let menu = [];
|
||||
|
||||
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
|
||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>);
|
||||
info.push(<span key='followed_by' className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>);
|
||||
} else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
|
||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
|
||||
info.push(<span key='blocked' className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
|
||||
}
|
||||
|
||||
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
|
||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
|
||||
info.push(<span key='muted' className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
|
||||
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
|
||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>);
|
||||
info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>);
|
||||
}
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
|
|
@ -110,7 +109,7 @@ class Header extends ImmutablePureComponent {
|
|||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
|
||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
|
||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
||||
}
|
||||
|
|
@ -234,8 +233,20 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='account__header__extra'>
|
||||
<div className='account__header__bio'>
|
||||
{fields.size > 0 && (
|
||||
{ (fields.size > 0 || identity_proofs.size > 0) && (
|
||||
<div className='account__header__fields'>
|
||||
{identity_proofs.map((proof, i) => (
|
||||
<dl key={i}>
|
||||
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
|
||||
|
||||
<dd className='verified'>
|
||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
||||
<Icon id='check' className='verified__mark' />
|
||||
</span></a>
|
||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
{fields.map((pair, i) => (
|
||||
<dl key={i}>
|
||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
|
||||
|
|
|
|||
|
|
@ -1,49 +1,140 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import { displayMedia } from '../../../initial_state';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
import { isIOS } from 'mastodon/is_mobile';
|
||||
|
||||
export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
attachment: ImmutablePropTypes.map.isRequired,
|
||||
displayWidth: PropTypes.number.isRequired,
|
||||
onOpenMedia: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
|
||||
visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
if (!this.state.visible) {
|
||||
this.setState({ visible: true });
|
||||
return true;
|
||||
componentDidMount () {
|
||||
if (this.props.attachment.get('blurhash')) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
_decode () {
|
||||
const hash = this.props.attachment.get('blurhash');
|
||||
const pixels = decode(hash, 32, 32);
|
||||
|
||||
if (pixels) {
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, 32, 32);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
}
|
||||
|
||||
handleImageLoad = () => {
|
||||
this.setState({ loaded: true });
|
||||
}
|
||||
|
||||
handleMouseEnter = e => {
|
||||
if (this.hoverToPlay()) {
|
||||
e.target.play();
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave = e => {
|
||||
if (this.hoverToPlay()) {
|
||||
e.target.pause();
|
||||
e.target.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
hoverToPlay () {
|
||||
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.visible) {
|
||||
this.props.onOpenMedia(this.props.attachment);
|
||||
} else {
|
||||
this.setState({ visible: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media } = this.props;
|
||||
const { visible } = this.state;
|
||||
const status = media.get('status');
|
||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
const style = {};
|
||||
const { attachment, displayWidth } = this.props;
|
||||
const { visible, loaded } = this.state;
|
||||
|
||||
let label, icon;
|
||||
const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
|
||||
const height = width;
|
||||
const status = attachment.get('status');
|
||||
const title = status.get('spoiler_text') || attachment.get('description');
|
||||
|
||||
if (media.get('type') === 'gifv') {
|
||||
label = <span className='media-gallery__gifv__label'>GIF</span>;
|
||||
let thumbnail = '';
|
||||
let icon;
|
||||
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
// Skip
|
||||
} else if (attachment.get('type') === 'image') {
|
||||
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
|
||||
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
|
||||
thumbnail = (
|
||||
<img
|
||||
src={attachment.get('preview_url')}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
/>
|
||||
);
|
||||
} else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
|
||||
const autoPlay = !isIOS() && autoPlayGif;
|
||||
|
||||
thumbnail = (
|
||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||
<video
|
||||
className='media-gallery__item-gifv-thumbnail'
|
||||
aria-label={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
role='application'
|
||||
src={attachment.get('url')}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
autoPlay={autoPlay}
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
|
||||
<span className='media-gallery__gifv__label'>GIF</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
style.backgroundImage = `url(${media.get('preview_url')})`;
|
||||
style.backgroundPosition = `${x}% ${y}%`;
|
||||
} else {
|
||||
if (!visible) {
|
||||
icon = (
|
||||
<span className='account-gallery__item__icons'>
|
||||
<Icon id='eye-slash' />
|
||||
|
|
@ -52,11 +143,12 @@ export default class MediaItem extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='account-gallery__item'>
|
||||
<Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style} onInterceptClick={this.handleClick}>
|
||||
{icon}
|
||||
{label}
|
||||
</Permalink>
|
||||
<div className='account-gallery__item' style={{ width, height }}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
|
||||
{visible && thumbnail}
|
||||
{!visible && icon}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,25 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { fetchAccount } from '../../actions/accounts';
|
||||
import { fetchAccount } from 'mastodon/actions/accounts';
|
||||
import { expandAccountMediaTimeline } from '../../actions/timelines';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnBackButton from '../../components/column_back_button';
|
||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { getAccountGallery } from '../../selectors';
|
||||
import { getAccountGallery } from 'mastodon/selectors';
|
||||
import MediaItem from './components/media_item';
|
||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||
import { ScrollContainer } from 'react-router-scroll-4';
|
||||
import LoadMore from '../../components/load_more';
|
||||
import LoadMore from 'mastodon/components/load_more';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
medias: getAccountGallery(state, props.params.accountId),
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
attachments: getAccountGallery(state, props.params.accountId),
|
||||
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||
});
|
||||
|
||||
class LoadMoreMedia extends ImmutablePureComponent {
|
||||
|
|
@ -49,9 +52,14 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
medias: ImmutablePropTypes.list.isRequired,
|
||||
attachments: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
width: 323,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
|
|
@ -68,11 +76,11 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
|
||||
handleScrollToBottom = () => {
|
||||
if (this.props.hasMore) {
|
||||
this.handleLoadMore(this.props.medias.size > 0 ? this.props.medias.last().getIn(['status', 'id']) : undefined);
|
||||
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll = (e) => {
|
||||
handleScroll = e => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
|
|
@ -85,17 +93,41 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId }));
|
||||
};
|
||||
|
||||
handleLoadOlder = (e) => {
|
||||
handleLoadOlder = e => {
|
||||
e.preventDefault();
|
||||
this.handleScrollToBottom();
|
||||
}
|
||||
|
||||
handleOpenMedia = attachment => {
|
||||
if (attachment.get('type') === 'video') {
|
||||
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') }));
|
||||
} else {
|
||||
const media = attachment.getIn(['status', 'media_attachments']);
|
||||
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
|
||||
|
||||
this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') }));
|
||||
}
|
||||
}
|
||||
|
||||
handleRef = c => {
|
||||
if (c) {
|
||||
this.setState({ width: c.offsetWidth });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { medias, shouldUpdateScroll, isLoading, hasMore } = this.props;
|
||||
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props;
|
||||
const { width } = this.state;
|
||||
|
||||
let loadOlder = null;
|
||||
if (!isAccount) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!medias && isLoading) {
|
||||
if (!attachments && isLoading) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
|
|
@ -103,7 +135,9 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
if (hasMore && !(isLoading && medias.size === 0)) {
|
||||
let loadOlder = null;
|
||||
|
||||
if (hasMore && !(isLoading && attachments.size === 0)) {
|
||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
||||
}
|
||||
|
||||
|
|
@ -115,23 +149,17 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
|
||||
<div role='feed' className='account-gallery__container'>
|
||||
{medias.map((media, index) => media === null ? (
|
||||
<LoadMoreMedia
|
||||
key={'more:' + medias.getIn(index + 1, 'id')}
|
||||
maxId={index > 0 ? medias.getIn(index - 1, 'id') : null}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
/>
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem
|
||||
key={media.get('id')}
|
||||
media={media}
|
||||
/>
|
||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||
))}
|
||||
|
||||
{loadOlder}
|
||||
</div>
|
||||
|
||||
{isLoading && medias.size === 0 && (
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import InnerHeader from '../../account/components/header';
|
||||
import MissingIndicator from '../../../components/missing_indicator';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import MovedNote from './moved_note';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
|
@ -12,6 +11,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
identity_proofs: ImmutablePropTypes.list,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
|
|
@ -84,10 +84,10 @@ export default class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { account, hideTabs } = this.props;
|
||||
const { account, hideTabs, identity_proofs } = this.props;
|
||||
|
||||
if (account === null) {
|
||||
return <MissingIndicator />;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -96,6 +96,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
|
||||
<InnerHeader
|
||||
account={account}
|
||||
identity_proofs={identity_proofs}
|
||||
onFollow={this.handleFollow}
|
||||
onBlock={this.handleBlock}
|
||||
onMention={this.handleMention}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { openModal } from '../../../actions/modal';
|
|||
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { unfollowModal } from '../../../initial_state';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
|
|
@ -35,6 +36,7 @@ const makeMapStateToProps = () => {
|
|||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
|
|
|||
|
|
@ -12,15 +12,21 @@ import ColumnBackButton from '../../components/column_back_button';
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
|
||||
const emptyList = ImmutableList();
|
||||
|
||||
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
|
||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
||||
|
||||
return {
|
||||
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
|
||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
|
||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
|
||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -36,24 +42,32 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
withReplies: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { accountId }, withReplies } = this.props;
|
||||
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(accountId));
|
||||
|
||||
if (!withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
}
|
||||
|
||||
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
|
||||
|
||||
if (!nextProps.withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
|
||||
}
|
||||
|
||||
this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
|
||||
}
|
||||
}
|
||||
|
|
@ -63,7 +77,15 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore } = this.props;
|
||||
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!statusIds && isLoading) {
|
||||
return (
|
||||
|
|
@ -73,6 +95,8 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
|
|
@ -81,13 +105,13 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
||||
alwaysPrepend
|
||||
scrollKey='account_timeline'
|
||||
statusIds={statusIds}
|
||||
statusIds={blockedBy ? emptyList : statusIds}
|
||||
featuredStatusIds={featuredStatusIds}
|
||||
isLoading={isLoading}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />}
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class ActionBar extends React.PureComponent {
|
|||
return (
|
||||
<div className='compose__action-bar'>
|
||||
<div className='compose__action-bar-dropdown'>
|
||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
<DropdownMenuContainer items={menu} icon='chevron-down' size={16} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ 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 AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import PollFormContainer from '../containers/poll_form_container';
|
||||
import UploadFormContainer from '../containers/upload_form_container';
|
||||
|
|
@ -40,17 +40,16 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
suggestion_token: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
spoiler: PropTypes.bool,
|
||||
privacy: PropTypes.string,
|
||||
spoiler_text: PropTypes.string,
|
||||
spoilerText: PropTypes.string,
|
||||
focusDate: PropTypes.instanceOf(Date),
|
||||
caretPosition: PropTypes.number,
|
||||
preselectDate: PropTypes.instanceOf(Date),
|
||||
is_submitting: PropTypes.bool,
|
||||
is_changing_upload: PropTypes.bool,
|
||||
is_uploading: PropTypes.bool,
|
||||
isSubmitting: PropTypes.bool,
|
||||
isChangingUpload: PropTypes.bool,
|
||||
isUploading: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
|
|
@ -85,10 +84,10 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
// Submit disabled:
|
||||
const { is_submitting, is_changing_upload, is_uploading, anyMedia } = this.props;
|
||||
const fulltext = [this.props.spoiler_text, countableText(this.props.text)].join('');
|
||||
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
|
||||
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||
|
||||
if (is_submitting || is_uploading || is_changing_upload || length(fulltext) > 10000 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > 10000 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -104,13 +103,23 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value);
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
|
||||
}
|
||||
|
||||
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
|
||||
}
|
||||
|
||||
handleChangeSpoilerText = (e) => {
|
||||
this.props.onChangeSpoilerText(e.target.value);
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
if (this.composeForm) {
|
||||
this.composeForm.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
// This statement does several things:
|
||||
// - If we're beginning a reply, and,
|
||||
|
|
@ -133,11 +142,11 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
} else if(prevProps.is_submitting && !this.props.is_submitting) {
|
||||
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||
if (this.props.spoiler) {
|
||||
this.spoilerText.focus();
|
||||
this.spoilerText.input.focus();
|
||||
} else {
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
}
|
||||
|
|
@ -152,6 +161,10 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
this.spoilerText = c;
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.composeForm = c;
|
||||
};
|
||||
|
||||
handleEmojiPick = (data) => {
|
||||
const { text } = this.props;
|
||||
const position = this.autosuggestTextarea.textarea.selectionStart;
|
||||
|
|
@ -162,9 +175,9 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
render () {
|
||||
const { intl, onPaste, showSearch, anyMedia } = this.props;
|
||||
const disabled = this.props.is_submitting;
|
||||
const text = [this.props.spoiler_text, countableText(this.props.text)].join('');
|
||||
const disabledButton = disabled || this.props.is_uploading || this.props.is_changing_upload || length(text) > 10000 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
|
||||
const disabled = this.props.isSubmitting;
|
||||
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 10000 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
|
||||
let publishText = '';
|
||||
|
||||
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||
|
|
@ -174,48 +187,59 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='compose-form'>
|
||||
<div className='compose-form' ref={this.setRef}>
|
||||
<WarningContainer />
|
||||
|
||||
<ReplyIndicatorContainer />
|
||||
|
||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
|
||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='compose-form__autosuggest-wrapper'>
|
||||
<AutosuggestTextarea
|
||||
ref={this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||
value={this.props.spoilerText}
|
||||
onChange={this.handleChangeSpoilerText}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
disabled={!this.props.spoiler}
|
||||
ref={this.setSpoilerText}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth)}
|
||||
onSuggestionSelected={this.onSpoilerSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
id='cw-spoiler-input'
|
||||
className='spoiler-input__input'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`emoji-picker-wrapper ${this.props.showSearch ? 'emoji-picker-wrapper--hidden' : ''}`}>
|
||||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||
</div>
|
||||
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<PollFormContainer />
|
||||
</div>
|
||||
<AutosuggestTextarea
|
||||
ref={this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onFocus={this.handleFocus}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth)}
|
||||
>
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<PollFormContainer />
|
||||
</div>
|
||||
</AutosuggestTextarea>
|
||||
|
||||
<div className='compose-form__buttons-wrapper'>
|
||||
<div className='compose-form__buttons'>
|
||||
<UploadButtonContainer />
|
||||
<PollButtonContainer />
|
||||
<PrivacyDropdownContainer />
|
||||
<SensitiveButtonContainer />
|
||||
<SpoilerButtonContainer />
|
||||
</div>
|
||||
<div className='character-counter__wrapper'><CharacterCounter max={10000} text={text} /></div>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ class ModifierPickerMenu extends React.PureComponent {
|
|||
active: PropTypes.bool,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
modifier: PropTypes.number,
|
||||
};
|
||||
|
||||
handleClick = e => {
|
||||
|
|
@ -87,36 +86,20 @@ class ModifierPickerMenu extends React.PureComponent {
|
|||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
if (this.node) {
|
||||
this.node.querySelector('li:first-child button').focus(); // focus the first element when opened
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { active, modifier } = this.props;
|
||||
const { active } = this.props;
|
||||
|
||||
return (
|
||||
<ul
|
||||
className='emoji-picker-dropdown__modifiers__menu'
|
||||
style={{ display: active ? 'block' : 'none' }}
|
||||
role='menuitem'
|
||||
ref={this.setRef}
|
||||
>
|
||||
{[1, 2, 3, 4, 5, 6].map(i => (
|
||||
<li
|
||||
onClick={this.handleClick}
|
||||
role='menuitemradio'
|
||||
aria-checked={i === (modifier || 1)}
|
||||
data-index={i}
|
||||
key={i}
|
||||
>
|
||||
<Emoji
|
||||
emoji='fist' set='twitter' size={22} sheetSize={32} skin={i}
|
||||
backgroundImageFn={backgroundImageFn}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
||||
<button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -148,22 +131,10 @@ class ModifierPicker extends React.PureComponent {
|
|||
render () {
|
||||
const { active, modifier } = this.props;
|
||||
|
||||
function setRef(ref) {
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
// TODO: It would be nice if we could pass props directly to emoji-mart's buttons.
|
||||
const button = ref.querySelector('button');
|
||||
button.setAttribute('aria-haspopup', 'true');
|
||||
button.setAttribute('aria-expanded', active);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown__modifiers'>
|
||||
<div ref={setRef}>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
|
||||
</div>
|
||||
<ModifierPickerMenu active={active} modifier={modifier} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
|
||||
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default class NavigationBar extends ImmutablePureComponent {
|
|||
<div className='navigation-bar'>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
||||
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
|
||||
<Avatar account={this.props.account} size={40} />
|
||||
<Avatar account={this.props.account} size={48} />
|
||||
</Permalink>
|
||||
|
||||
<div className='navigation-bar__profile'>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import AutosuggestInput from 'mastodon/components/autosuggest_input';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
@ -26,6 +27,11 @@ class Option extends React.PureComponent {
|
|||
isPollMultiple: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
|
@ -37,20 +43,48 @@ class Option extends React.PureComponent {
|
|||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested = (token) => {
|
||||
this.props.onFetchSuggestions(token);
|
||||
}
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<label className='poll__text editable'>
|
||||
<span className={classNames('poll__input', { checkbox: isPollMultiple })} />
|
||||
<span
|
||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
||||
onClick={this.handleToggleMultiple}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
/>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={25}
|
||||
value={title}
|
||||
onChange={this.handleOptionTitleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
|
@ -75,6 +109,10 @@ class PollForm extends ImmutablePureComponent {
|
|||
onAddOption: PropTypes.func.isRequired,
|
||||
onRemoveOption: PropTypes.func.isRequired,
|
||||
onChangeSettings: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
|
@ -86,8 +124,12 @@ class PollForm extends ImmutablePureComponent {
|
|||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
||||
};
|
||||
|
||||
handleToggleMultiple = () => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
|
||||
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
|
|
@ -96,7 +138,7 @@ class PollForm extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} />)}
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class SearchPopout extends React.PureComponent {
|
|||
const { style } = this.props;
|
||||
const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
|
||||
return (
|
||||
<div style={{ ...style, position: 'absolute', width: 285 }}>
|
||||
<div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}>
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
|
|
@ -47,6 +47,10 @@ class SearchPopout extends React.PureComponent {
|
|||
export default @injectIntl
|
||||
class Search extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
submitted: PropTypes.bool,
|
||||
|
|
@ -54,6 +58,7 @@ class Search extends React.PureComponent {
|
|||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func.isRequired,
|
||||
openInRoute: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
|
@ -73,19 +78,20 @@ class Search extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
handleKeyUp = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onSubmit();
|
||||
|
||||
if (this.props.openInRoute) {
|
||||
this.context.router.history.push('/search');
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
document.querySelector('.ui').parentElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
noop () {
|
||||
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ expanded: true });
|
||||
this.props.onShow();
|
||||
|
|
@ -110,7 +116,7 @@ class Search extends React.PureComponent {
|
|||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import UploadContainer from '../containers/upload_container';
|
||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||
|
||||
export default class UploadForm extends ImmutablePureComponent {
|
||||
|
||||
|
|
@ -22,6 +23,8 @@ export default class UploadForm extends ImmutablePureComponent {
|
|||
<UploadContainer id={id} key={id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!mediaIds.isEmpty() && <SensitiveButtonContainer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ComposeForm from '../components/compose_form';
|
||||
import { uploadCompose } from '../../../actions/compose';
|
||||
import {
|
||||
changeCompose,
|
||||
submitCompose,
|
||||
|
|
@ -9,21 +8,21 @@ import {
|
|||
selectComposeSuggestion,
|
||||
changeComposeSpoilerText,
|
||||
insertEmojiCompose,
|
||||
uploadCompose,
|
||||
} from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
text: state.getIn(['compose', 'text']),
|
||||
suggestion_token: state.getIn(['compose', 'suggestion_token']),
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
spoiler: state.getIn(['compose', 'spoiler']),
|
||||
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
||||
spoilerText: state.getIn(['compose', 'spoiler_text']),
|
||||
privacy: state.getIn(['compose', 'privacy']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||
is_changing_upload: state.getIn(['compose', 'is_changing_upload']),
|
||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
});
|
||||
|
|
@ -46,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
dispatch(fetchComposeSuggestions(token));
|
||||
},
|
||||
|
||||
onSuggestionSelected (position, token, accountId) {
|
||||
dispatch(selectComposeSuggestion(position, token, accountId));
|
||||
onSuggestionSelected (position, token, suggestion, path) {
|
||||
dispatch(selectComposeSuggestion(position, token, suggestion, path));
|
||||
},
|
||||
|
||||
onChangeSpoilerText (checked) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PollForm from '../components/poll_form';
|
||||
import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
|
||||
import {
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
} from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
options: state.getIn(['compose', 'poll', 'options']),
|
||||
expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
|
||||
isMultiple: state.getIn(['compose', 'poll', 'multiple']),
|
||||
|
|
@ -24,6 +30,19 @@ const mapDispatchToProps = dispatch => ({
|
|||
onChangeSettings(expiresIn, isMultiple) {
|
||||
dispatch(changePollSettings(expiresIn, isMultiple));
|
||||
},
|
||||
|
||||
onClearSuggestions () {
|
||||
dispatch(clearComposeSuggestions());
|
||||
},
|
||||
|
||||
onFetchSuggestions (token) {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
},
|
||||
|
||||
onSuggestionSelected (position, token, accountId, path) {
|
||||
dispatch(selectComposeSuggestion(position, token, accountId, path));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,8 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { changeComposeSensitivity } from '../../../actions/compose';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { changeComposeSensitivity } from 'mastodon/actions/compose';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
|
||||
|
|
@ -14,7 +11,6 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
visible: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
active: state.getIn(['compose', 'sensitive']),
|
||||
disabled: state.getIn(['compose', 'spoiler']),
|
||||
});
|
||||
|
|
@ -30,7 +26,6 @@ const mapDispatchToProps = dispatch => ({
|
|||
class SensitiveButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
|
|
@ -38,32 +33,24 @@ class SensitiveButton extends React.PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { visible, active, disabled, onClick, intl } = this.props;
|
||||
const { active, disabled, onClick, intl } = this.props;
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}>
|
||||
{({ scale }) => {
|
||||
const icon = active ? 'eye-slash' : 'eye';
|
||||
const className = classNames('compose-form__sensitive-button', {
|
||||
'compose-form__sensitive-button--visible': visible,
|
||||
});
|
||||
return (
|
||||
<div className={className} style={{ transform: `scale(${scale})` }}>
|
||||
<IconButton
|
||||
className='compose-form__sensitive-button__icon'
|
||||
title={intl.formatMessage(active ? messages.marked : messages.unmarked)}
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
size={18}
|
||||
active={active}
|
||||
disabled={disabled}
|
||||
style={{ lineHeight: null, height: null }}
|
||||
inverted
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Motion>
|
||||
<div className='compose-form__sensitive-button'>
|
||||
<label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
|
||||
<input
|
||||
name='mark-sensitive'
|
||||
type='checkbox'
|
||||
checked={active}
|
||||
onChange={onClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<span className={classNames('checkbox', { active })} />
|
||||
|
||||
<FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,12 +106,12 @@ class Compose extends React.PureComponent {
|
|||
<div className='drawer__pager'>
|
||||
{!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
|
||||
<ComposeFormContainer />
|
||||
{multiColumn && (
|
||||
<div className='drawer__inner__mastodon'>
|
||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='drawer__inner__mastodon'>
|
||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingText from '../../../components/setting_text';
|
||||
|
||||
const messages = defineMessages({
|
||||
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
|
||||
settings: { id: 'home.settings', defaultMessage: 'Column settings' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -20,18 +20,24 @@ export default class ConversationsList extends ImmutablePureComponent {
|
|||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) - 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) + 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
if (align_top && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ColumnSettings from '../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);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import Picker from 'emoji-mart/dist-modern/components/picker/picker';
|
||||
import Emoji from 'emoji-mart/dist-modern/components/emoji/emoji';
|
||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
||||
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
|
||||
|
||||
export {
|
||||
Picker,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column icon='users' heading={intl.formatMessage(messages.heading)}>
|
||||
<Column icon='user-plus' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<ScrollableList
|
||||
scrollKey='follow_requests'
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@ import Column from '../ui/components/column';
|
|||
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||
import ColumnBackButton from '../../components/column_back_button';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
|
||||
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
|
@ -31,6 +34,8 @@ class Followers extends ImmutablePureComponent {
|
|||
shouldUpdateScroll: PropTypes.func,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
|
|
@ -50,7 +55,15 @@ class Followers extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { shouldUpdateScroll, accountIds, hasMore } = this.props;
|
||||
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
|
|
@ -60,7 +73,7 @@ class Followers extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
|
||||
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
|
|
@ -75,7 +88,7 @@ class Followers extends ImmutablePureComponent {
|
|||
alwaysPrepend
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
{blockedBy ? [] : accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@ import Column from '../ui/components/column';
|
|||
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||
import ColumnBackButton from '../../components/column_back_button';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
|
||||
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
|
@ -31,6 +34,8 @@ class Following extends ImmutablePureComponent {
|
|||
shouldUpdateScroll: PropTypes.func,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
|
|
@ -50,7 +55,15 @@ class Following extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { shouldUpdateScroll, accountIds, hasMore } = this.props;
|
||||
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
|
|
@ -60,7 +73,7 @@ class Following extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
|
||||
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
|
|
@ -75,7 +88,7 @@ class Following extends ImmutablePureComponent {
|
|||
alwaysPrepend
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
{blockedBy ? [] : accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ import { connect } from 'react-redux';
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me, invitesEnabled, version, profile_directory } from '../../initial_state';
|
||||
import { fetchFollowRequests } from '../../actions/accounts';
|
||||
import { me, profile_directory } from '../../initial_state';
|
||||
import { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { Link } from 'react-router-dom';
|
||||
import NavigationBar from '../compose/components/navigation_bar';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
||||
|
||||
const messages = defineMessages({
|
||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||
|
|
@ -55,10 +55,16 @@ const badgeDisplay = (number, limit) => {
|
|||
}
|
||||
};
|
||||
|
||||
const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class GettingStarted extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
myAccount: ImmutablePropTypes.map.isRequired,
|
||||
|
|
@ -70,7 +76,12 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { myAccount, fetchFollowRequests } = this.props;
|
||||
const { myAccount, fetchFollowRequests, multiColumn } = this.props;
|
||||
|
||||
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
|
||||
this.context.router.history.replace('/timelines/home');
|
||||
return;
|
||||
}
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
fetchFollowRequests();
|
||||
|
|
@ -123,7 +134,7 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
height += 48*3;
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
navItems.push(<ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
height += 48;
|
||||
}
|
||||
|
||||
|
|
@ -155,27 +166,7 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
|
||||
{!multiColumn && <div className='flex-spacer' />}
|
||||
|
||||
<div className='getting-started__footer'>
|
||||
<ul>
|
||||
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
|
||||
{multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
|
||||
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
|
||||
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
|
||||
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
|
||||
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
|
||||
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
|
||||
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
|
||||
<li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
||||
values={{ github: <span><a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>tootsuite/mastodon</a> (v{version})</span> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<LinkFooter withHotkeys={multiColumn} />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
|
|||
<td><kbd>x</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>h</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>up</kbd>, <kbd>k</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,23 @@ class ListTimeline extends React.PureComponent {
|
|||
this.disconnect = dispatch(connectListStream(id));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = nextProps.params;
|
||||
|
||||
if (id !== this.props.params.id) {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
|
||||
dispatch(fetchList(id));
|
||||
dispatch(expandListTimeline(id));
|
||||
|
||||
this.disconnect = dispatch(connectListStream(id));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
|
|
@ -158,8 +175,6 @@ class ListTimeline extends React.PureComponent {
|
|||
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue