Merge remote-tracking branch 'glitchsoc/master'
commit
bd942ac2a5
|
@ -177,8 +177,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- *attach_workspace
|
||||||
- run: bundle exec i18n-tasks check-normalized
|
- run: bundle exec i18n-tasks check-normalized
|
||||||
- run: bundle exec i18n-tasks unused
|
- run: bundle exec i18n-tasks unused -l en
|
||||||
- run: bundle exec i18n-tasks missing -t plural
|
|
||||||
- run: bundle exec i18n-tasks check-consistent-interpolations
|
- run: bundle exec i18n-tasks check-consistent-interpolations
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
|
|
|
@ -30,8 +30,8 @@ plugins:
|
||||||
channel: eslint-5
|
channel: eslint-5
|
||||||
rubocop:
|
rubocop:
|
||||||
enabled: true
|
enabled: true
|
||||||
channel: rubocop-0-54
|
channel: rubocop-0-71
|
||||||
scss-lint:
|
sass-lint:
|
||||||
enabled: true
|
enabled: true
|
||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
- spec/
|
- spec/
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
update_configs:
|
||||||
|
- package_manager: "ruby:bundler"
|
||||||
|
directory: "/"
|
||||||
|
update_schedule: "weekly"
|
||||||
|
|
||||||
|
- package_manager: "javascript"
|
||||||
|
directory: "/"
|
||||||
|
update_schedule: "weekly"
|
|
@ -160,6 +160,21 @@ STREAMING_CLUSTER_NUM=1
|
||||||
# Maximum number of pinned posts
|
# Maximum number of pinned posts
|
||||||
# MAX_PINNED_TOOTS=5
|
# MAX_PINNED_TOOTS=5
|
||||||
|
|
||||||
|
# Maximum allowed bio characters
|
||||||
|
# MAX_BIO_CHARS=500
|
||||||
|
|
||||||
|
# Maximim number of profile fields allowed
|
||||||
|
# MAX_PROFILE_FIELDS=4
|
||||||
|
|
||||||
|
# Maximum allowed display name characters
|
||||||
|
# MAX_DISPLAY_NAME_CHARS=30
|
||||||
|
|
||||||
|
# Maximum image and video/audio upload sizes
|
||||||
|
# Units are in bytes
|
||||||
|
# 1048576 bytes equals 1 megabyte
|
||||||
|
# MAX_IMAGE_SIZE=8388608
|
||||||
|
# MAX_VIDEO_SIZE=41943040
|
||||||
|
|
||||||
# LDAP authentication (optional)
|
# LDAP authentication (optional)
|
||||||
# LDAP_ENABLED=true
|
# LDAP_ENABLED=true
|
||||||
# LDAP_HOST=localhost
|
# LDAP_HOST=localhost
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
patreon: mastodon
|
||||||
|
open_collective: mastodon
|
|
@ -1,3 +1,6 @@
|
||||||
|
require:
|
||||||
|
- rubocop-rails
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
TargetRubyVersion: 2.3
|
TargetRubyVersion: 2.3
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -82,6 +85,9 @@ Rails/Exit:
|
||||||
- 'lib/mastodon/*'
|
- 'lib/mastodon/*'
|
||||||
- 'lib/cli.rb'
|
- 'lib/cli.rb'
|
||||||
|
|
||||||
|
Rails/HelperInstanceVariable:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Style/ClassAndModuleChildren:
|
Style/ClassAndModuleChildren:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
|
|
@ -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
|
# for specific ignore
|
||||||
!.svgo.yml
|
!.svgo.yml
|
||||||
|
!sass-lint/**/*.yml
|
||||||
|
|
126
CHANGELOG.md
126
CHANGELOG.md
|
@ -3,6 +3,132 @@ Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [2.9.2] - 2019-06-22
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add `short_description` and `approval_required` to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/11146))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change camera icon to paperclip icon in upload form ([koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/11149))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix audio-only OGG and WebM files not being processed as such ([Gargron](https://github.com/tootsuite/mastodon/pull/11151))
|
||||||
|
- Fix audio not being downloaded from remote servers ([Gargron](https://github.com/tootsuite/mastodon/pull/11145))
|
||||||
|
|
||||||
|
## [2.9.1] - 2019-06-22
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add moderation API ([Gargron](https://github.com/tootsuite/mastodon/pull/9387))
|
||||||
|
- Add audio uploads ([Gargron](https://github.com/tootsuite/mastodon/pull/11123), [Gargron](https://github.com/tootsuite/mastodon/pull/11141))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change domain blocks to automatically support subdomains ([Gargron](https://github.com/tootsuite/mastodon/pull/11138))
|
||||||
|
- Change Nanobox configuration to bring it up to date ([danhunsaker](https://github.com/tootsuite/mastodon/pull/11083))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Remove expensive counters from federation page in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11139))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix converted media being saved with original extension and mime type ([Gargron](https://github.com/tootsuite/mastodon/pull/11130))
|
||||||
|
- Fix layout of identity proofs settings ([acid-chicken](https://github.com/tootsuite/mastodon/pull/11126))
|
||||||
|
- Fix active scope only returning suspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/11111))
|
||||||
|
- Fix sanitizer making block level elements unreadable ([Gargron](https://github.com/tootsuite/mastodon/pull/10836))
|
||||||
|
- Fix label for site theme not being translated in admin UI ([palindromordnilap](https://github.com/tootsuite/mastodon/pull/11121))
|
||||||
|
- Fix statuses not being filtered irreversibly in web UI under some circumstances ([ThibG](https://github.com/tootsuite/mastodon/pull/11113))
|
||||||
|
- Fix scrolling behaviour in compose form ([ThibG](https://github.com/tootsuite/mastodon/pull/11093))
|
||||||
|
|
||||||
|
## [2.9.0] - 2019-06-13
|
||||||
|
### 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))
|
||||||
|
- Add button to conveniently copy OAuth code ([ThibG](https://github.com/tootsuite/mastodon/pull/11065))
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
- Fix list not being automatically unpinned when it returns 404 in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11045))
|
||||||
|
- Fix login sometimes redirecting to paths that are not pages ([Gargron](https://github.com/tootsuite/mastodon/pull/11019))
|
||||||
|
|
||||||
|
## [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
|
## [2.8.2] - 2019-05-05
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beatrix.bitrot@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at glitch-abuse@sitedethib.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,9 @@ Bug reports and feature suggestions can be submitted to [GitHub Issues](https://
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
You can submit translations via pull request.
|
You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase.
|
||||||
|
|
||||||
|
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
|
||||||
|
|
||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
|
|
10
Dockerfile
10
Dockerfile
|
@ -7,7 +7,6 @@ SHELL ["bash", "-c"]
|
||||||
ENV NODE_VER="8.15.0"
|
ENV NODE_VER="8.15.0"
|
||||||
RUN echo "Etc/UTC" > /etc/localtime && \
|
RUN echo "Etc/UTC" > /etc/localtime && \
|
||||||
apt update && \
|
apt update && \
|
||||||
apt -y dist-upgrade && \
|
|
||||||
apt -y install wget make gcc g++ python && \
|
apt -y install wget make gcc g++ python && \
|
||||||
cd ~ && \
|
cd ~ && \
|
||||||
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \
|
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 && \
|
RUN apt update && \
|
||||||
echo "Etc/UTC" > /etc/localtime && \
|
echo "Etc/UTC" > /etc/localtime && \
|
||||||
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
|
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
|
||||||
apt -y dist-upgrade && \
|
|
||||||
apt install -y whois wget && \
|
apt install -y whois wget && \
|
||||||
addgroup --gid $GID mastodon && \
|
addgroup --gid $GID mastodon && \
|
||||||
useradd -m -u $UID -g $GID -d /opt/mastodon 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
|
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 \
|
RUN apt -y --no-install-recommends install \
|
||||||
libssl1.1 libpq5 imagemagick ffmpeg \
|
libssl1.1 libpq5 imagemagick ffmpeg \
|
||||||
libicu60 libprotobuf10 libidn11 libyaml-0-2 \
|
libicu60 libprotobuf10 libidn11 libyaml-0-2 \
|
||||||
|
@ -95,7 +93,7 @@ RUN apt -y --no-install-recommends install \
|
||||||
ln -s /opt/mastodon /mastodon && \
|
ln -s /opt/mastodon /mastodon && \
|
||||||
gem install bundler && \
|
gem install bundler && \
|
||||||
rm -rf /var/cache && \
|
rm -rf /var/cache && \
|
||||||
rm -rf /var/lib/apt
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Add tini
|
# Add tini
|
||||||
ENV TINI_VERSION="0.18.0"
|
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 echo "$TINI_SUM tini" | sha256sum -c -
|
||||||
RUN chmod +x /tini
|
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 --chown=mastodon:mastodon . /opt/mastodon
|
||||||
COPY --from=build-dep --chown=mastodon:mastodon /opt/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 RAILS_ENV="production"
|
||||||
ENV NODE_ENV="production"
|
ENV NODE_ENV="production"
|
||||||
|
|
||||||
|
|
23
Gemfile
23
Gemfile
|
@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
|
||||||
gem 'pghero', '~> 2.2'
|
gem 'pghero', '~> 2.2'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.39', require: false
|
gem 'aws-sdk-s3', '~> 1.45', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'paperclip', '~> 6.0'
|
gem 'paperclip', '~> 6.0'
|
||||||
|
@ -54,7 +54,7 @@ gem 'htmlentities', '~> 4.3'
|
||||||
gem 'http', '~> 3.3'
|
gem 'http', '~> 3.3'
|
||||||
gem 'http_accept_language', '~> 2.1'
|
gem 'http_accept_language', '~> 2.1'
|
||||||
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2'
|
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 'idn-ruby', require: 'idn'
|
||||||
gem 'kaminari', '~> 1.1'
|
gem 'kaminari', '~> 1.1'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
|
@ -63,7 +63,7 @@ gem 'nokogiri', '~> 1.10'
|
||||||
gem 'nsa', '~> 0.2'
|
gem 'nsa', '~> 0.2'
|
||||||
gem 'oj', '~> 3.7'
|
gem 'oj', '~> 3.7'
|
||||||
gem 'ostatus2', '~> 2.0'
|
gem 'ostatus2', '~> 2.0'
|
||||||
gem 'ox', '~> 2.10'
|
gem 'ox', '~> 2.11'
|
||||||
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
|
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
|
||||||
gem 'pundit', '~> 2.0'
|
gem 'pundit', '~> 2.0'
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
|
@ -83,9 +83,9 @@ gem 'simple-navigation', '~> 4.0'
|
||||||
gem 'simple_form', '~> 4.1'
|
gem 'simple_form', '~> 4.1'
|
||||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||||
gem 'stoplight', '~> 2.1.3'
|
gem 'stoplight', '~> 2.1.3'
|
||||||
gem 'strong_migrations', '~> 0.3'
|
gem 'strong_migrations', '~> 0.4'
|
||||||
gem 'tty-command', '~> 0.8', require: false
|
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 'twitter-text', '~> 1.14'
|
||||||
gem 'tzinfo-data', '~> 1.2019'
|
gem 'tzinfo-data', '~> 1.2019'
|
||||||
gem 'webpacker', '~> 4.0'
|
gem 'webpacker', '~> 4.0'
|
||||||
|
@ -99,7 +99,7 @@ gem 'redcarpet', '~> 3.4'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'fabrication', '~> 2.20'
|
gem 'fabrication', '~> 2.20'
|
||||||
gem 'fuubar', '~> 2.3'
|
gem 'fuubar', '~> 2.4'
|
||||||
gem 'i18n-tasks', '~> 0.9', require: false
|
gem 'i18n-tasks', '~> 0.9', require: false
|
||||||
gem 'pry-byebug', '~> 3.7'
|
gem 'pry-byebug', '~> 3.7'
|
||||||
gem 'pry-rails', '~> 0.3'
|
gem 'pry-rails', '~> 0.3'
|
||||||
|
@ -111,14 +111,14 @@ group :production, :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.20'
|
gem 'capybara', '~> 3.25'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 1.9'
|
gem 'faker', '~> 1.9'
|
||||||
gem 'microformats', '~> 4.1'
|
gem 'microformats', '~> 4.1'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.0'
|
gem 'rspec-sidekiq', '~> 3.0'
|
||||||
gem 'simplecov', '~> 0.16', require: false
|
gem 'simplecov', '~> 0.17', require: false
|
||||||
gem 'webmock', '~> 3.5'
|
gem 'webmock', '~> 3.6'
|
||||||
gem 'parallel_tests', '~> 2.29'
|
gem 'parallel_tests', '~> 2.29'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -131,10 +131,10 @@ group :development do
|
||||||
gem 'letter_opener', '~> 1.7'
|
gem 'letter_opener', '~> 1.7'
|
||||||
gem 'letter_opener_web', '~> 1.3'
|
gem 'letter_opener_web', '~> 1.3'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 0.69', require: false
|
gem 'rubocop', '~> 0.72', require: false
|
||||||
|
gem 'rubocop-rails', '~> 2.2', require: false
|
||||||
gem 'brakeman', '~> 4.5', require: false
|
gem 'brakeman', '~> 4.5', require: false
|
||||||
gem 'bundler-audit', '~> 0.6', require: false
|
gem 'bundler-audit', '~> 0.6', require: false
|
||||||
gem 'scss_lint', '~> 0.58', require: false
|
|
||||||
|
|
||||||
gem 'capistrano', '~> 3.11'
|
gem 'capistrano', '~> 3.11'
|
||||||
gem 'capistrano-rails', '~> 1.4'
|
gem 'capistrano-rails', '~> 1.4'
|
||||||
|
@ -151,3 +151,4 @@ group :production do
|
||||||
end
|
end
|
||||||
|
|
||||||
gem 'concurrent-ruby', require: false
|
gem 'concurrent-ruby', require: false
|
||||||
|
gem 'connection_pool', require: false
|
||||||
|
|
140
Gemfile.lock
140
Gemfile.lock
|
@ -76,19 +76,19 @@ GEM
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-eventstream (1.0.3)
|
aws-eventstream (1.0.3)
|
||||||
aws-partitions (1.162.0)
|
aws-partitions (1.184.0)
|
||||||
aws-sdk-core (3.52.1)
|
aws-sdk-core (3.59.0)
|
||||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||||
aws-partitions (~> 1.0)
|
aws-partitions (~> 1.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.20.0)
|
aws-sdk-kms (1.23.0)
|
||||||
aws-sdk-core (~> 3, >= 3.52.1)
|
aws-sdk-core (~> 3, >= 3.58.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.39.0)
|
aws-sdk-s3 (1.45.0)
|
||||||
aws-sdk-core (~> 3, >= 3.52.1)
|
aws-sdk-core (~> 3, >= 3.58.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.1.0)
|
aws-sigv4 (1.1.0)
|
||||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||||
bcrypt (3.1.12)
|
bcrypt (3.1.12)
|
||||||
|
@ -106,7 +106,7 @@ GEM
|
||||||
brakeman (4.5.1)
|
brakeman (4.5.1)
|
||||||
browser (2.5.3)
|
browser (2.5.3)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (6.0.0)
|
bullet (6.0.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
bundler-audit (0.6.1)
|
bundler-audit (0.6.1)
|
||||||
|
@ -129,14 +129,13 @@ GEM
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
capybara (3.20.0)
|
capybara (3.25.0)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
rack (>= 1.6.0)
|
rack (>= 1.6.0)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (~> 1.2)
|
regexp_parser (~> 1.5)
|
||||||
uglifier
|
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
case_transform (0.2)
|
case_transform (0.2)
|
||||||
activesupport
|
activesupport
|
||||||
|
@ -160,7 +159,7 @@ GEM
|
||||||
css_parser (1.6.0)
|
css_parser (1.6.0)
|
||||||
addressable
|
addressable
|
||||||
debug_inspector (0.0.3)
|
debug_inspector (0.0.3)
|
||||||
derailed_benchmarks (1.3.5)
|
derailed_benchmarks (1.3.6)
|
||||||
benchmark-ips (~> 2)
|
benchmark-ips (~> 2)
|
||||||
get_process_mem (~> 0)
|
get_process_mem (~> 0)
|
||||||
heapy (~> 0)
|
heapy (~> 0)
|
||||||
|
@ -184,14 +183,14 @@ GEM
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
rpam2 (~> 4.0)
|
rpam2 (~> 4.0)
|
||||||
diff-lcs (1.3)
|
diff-lcs (1.3)
|
||||||
docile (1.3.0)
|
docile (1.3.2)
|
||||||
domain_name (0.5.20180417)
|
domain_name (0.5.20180417)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (5.1.0)
|
doorkeeper (5.1.0)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (2.7.2)
|
dotenv (2.7.4)
|
||||||
dotenv-rails (2.7.2)
|
dotenv-rails (2.7.4)
|
||||||
dotenv (= 2.7.2)
|
dotenv (= 2.7.4)
|
||||||
railties (>= 3.2, < 6.1)
|
railties (>= 3.2, < 6.1)
|
||||||
elasticsearch (6.0.2)
|
elasticsearch (6.0.2)
|
||||||
elasticsearch-api (= 6.0.2)
|
elasticsearch-api (= 6.0.2)
|
||||||
|
@ -208,9 +207,8 @@ GEM
|
||||||
et-orbi (1.1.6)
|
et-orbi (1.1.6)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.62.0)
|
excon (0.62.0)
|
||||||
execjs (2.7.0)
|
|
||||||
fabrication (2.20.2)
|
fabrication (2.20.2)
|
||||||
faker (1.9.3)
|
faker (1.9.6)
|
||||||
i18n (>= 0.7)
|
i18n (>= 0.7)
|
||||||
faraday (0.15.0)
|
faraday (0.15.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
|
@ -233,7 +231,7 @@ GEM
|
||||||
fugit (1.1.6)
|
fugit (1.1.6)
|
||||||
et-orbi (~> 1.1, >= 1.1.6)
|
et-orbi (~> 1.1, >= 1.1.6)
|
||||||
raabro (~> 1.1)
|
raabro (~> 1.1)
|
||||||
fuubar (2.3.2)
|
fuubar (2.4.1)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
get_process_mem (0.2.3)
|
get_process_mem (0.2.3)
|
||||||
|
@ -255,7 +253,7 @@ GEM
|
||||||
railties (>= 4.0.1)
|
railties (>= 4.0.1)
|
||||||
hamster (3.0.0)
|
hamster (3.0.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
hashdiff (0.3.7)
|
hashdiff (0.4.0)
|
||||||
hashie (3.6.0)
|
hashie (3.6.0)
|
||||||
heapy (0.1.4)
|
heapy (0.1.4)
|
||||||
highline (2.0.1)
|
highline (2.0.1)
|
||||||
|
@ -273,7 +271,7 @@ GEM
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (2.1.1)
|
http-form_data (2.1.1)
|
||||||
http_accept_language (2.1.1)
|
http_accept_language (2.1.1)
|
||||||
httplog (1.2.2)
|
httplog (1.3.1)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.6.0)
|
i18n (1.6.0)
|
||||||
|
@ -291,9 +289,9 @@ GEM
|
||||||
idn-ruby (0.1.0)
|
idn-ruby (0.1.0)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
iso-639 (0.2.8)
|
iso-639 (0.2.8)
|
||||||
jaro_winkler (1.5.2)
|
jaro_winkler (1.5.3)
|
||||||
jmespath (1.4.0)
|
jmespath (1.4.0)
|
||||||
json (2.1.0)
|
json (2.2.0)
|
||||||
json-ld (3.0.2)
|
json-ld (3.0.2)
|
||||||
multi_json (~> 1.12)
|
multi_json (~> 1.12)
|
||||||
rdf (>= 2.2.8, < 4.0)
|
rdf (>= 2.2.8, < 4.0)
|
||||||
|
@ -324,7 +322,7 @@ GEM
|
||||||
letter_opener (~> 1.0)
|
letter_opener (~> 1.0)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
link_header (0.0.8)
|
link_header (0.0.8)
|
||||||
lograge (0.11.0)
|
lograge (0.11.2)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
|
@ -340,7 +338,7 @@ GEM
|
||||||
mimemagic (~> 0.3.2)
|
mimemagic (~> 0.3.2)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
memory_profiler (0.9.13)
|
memory_profiler (0.9.14)
|
||||||
method_source (0.9.2)
|
method_source (0.9.2)
|
||||||
microformats (4.1.0)
|
microformats (4.1.0)
|
||||||
json (~> 2.1)
|
json (~> 2.1)
|
||||||
|
@ -355,7 +353,7 @@ GEM
|
||||||
msgpack (1.2.10)
|
msgpack (1.2.10)
|
||||||
multi_json (1.13.1)
|
multi_json (1.13.1)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
necromancer (0.4.0)
|
necromancer (0.5.0)
|
||||||
net-ldap (0.16.1)
|
net-ldap (0.16.1)
|
||||||
net-scp (1.2.1)
|
net-scp (1.2.1)
|
||||||
net-ssh (>= 2.6.5)
|
net-ssh (>= 2.6.5)
|
||||||
|
@ -386,7 +384,7 @@ GEM
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
http (~> 3.0)
|
http (~> 3.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
ox (2.10.0)
|
ox (2.11.0)
|
||||||
paperclip (6.0.0)
|
paperclip (6.0.0)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
|
@ -397,7 +395,7 @@ GEM
|
||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parallel (1.17.0)
|
parallel (1.17.0)
|
||||||
parallel_tests (2.29.0)
|
parallel_tests (2.29.1)
|
||||||
parallel
|
parallel
|
||||||
parser (2.6.3.0)
|
parser (2.6.3.0)
|
||||||
ast (~> 2.4.0)
|
ast (~> 2.4.0)
|
||||||
|
@ -405,7 +403,7 @@ GEM
|
||||||
equatable (~> 0.5.0)
|
equatable (~> 0.5.0)
|
||||||
tty-color (~> 0.4.0)
|
tty-color (~> 0.4.0)
|
||||||
pg (1.1.4)
|
pg (1.1.4)
|
||||||
pghero (2.2.0)
|
pghero (2.2.1)
|
||||||
activerecord
|
activerecord
|
||||||
pkg-config (1.3.7)
|
pkg-config (1.3.7)
|
||||||
premailer (1.11.1)
|
premailer (1.11.1)
|
||||||
|
@ -424,7 +422,7 @@ GEM
|
||||||
pry (~> 0.10)
|
pry (~> 0.10)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (3.0.3)
|
public_suffix (3.1.1)
|
||||||
puma (3.12.1)
|
puma (3.12.1)
|
||||||
pundit (2.0.1)
|
pundit (2.0.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -474,16 +472,13 @@ GEM
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.19.0, < 2.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (12.3.2)
|
rake (12.3.2)
|
||||||
rb-fsevent (0.10.3)
|
|
||||||
rb-inotify (0.10.0)
|
|
||||||
ffi (~> 1.0)
|
|
||||||
rdf (3.0.9)
|
rdf (3.0.9)
|
||||||
hamster (~> 3.0)
|
hamster (~> 3.0)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.3.3)
|
rdf-normalize (0.3.3)
|
||||||
rdf (>= 2.2, < 4.0)
|
rdf (>= 2.2, < 4.0)
|
||||||
redcarpet (3.4.0)
|
redcarpet (3.4.0)
|
||||||
redis (4.1.1)
|
redis (4.1.2)
|
||||||
redis-actionpack (5.0.2)
|
redis-actionpack (5.0.2)
|
||||||
actionpack (>= 4.0, < 6)
|
actionpack (>= 4.0, < 6)
|
||||||
redis-rack (>= 1, < 3)
|
redis-rack (>= 1, < 3)
|
||||||
|
@ -502,7 +497,7 @@ GEM
|
||||||
redis-store (>= 1.2, < 2)
|
redis-store (>= 1.2, < 2)
|
||||||
redis-store (1.5.0)
|
redis-store (1.5.0)
|
||||||
redis (>= 2.2, < 5)
|
redis (>= 2.2, < 5)
|
||||||
regexp_parser (1.5.0)
|
regexp_parser (1.5.1)
|
||||||
request_store (1.4.1)
|
request_store (1.4.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (2.4.1)
|
responders (2.4.1)
|
||||||
|
@ -532,31 +527,26 @@ GEM
|
||||||
rspec-core (~> 3.0, >= 3.0.0)
|
rspec-core (~> 3.0, >= 3.0.0)
|
||||||
sidekiq (>= 2.4.0)
|
sidekiq (>= 2.4.0)
|
||||||
rspec-support (3.8.0)
|
rspec-support (3.8.0)
|
||||||
rubocop (0.69.0)
|
rubocop (0.72.0)
|
||||||
jaro_winkler (~> 1.5.1)
|
jaro_winkler (~> 1.5.1)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.6)
|
parser (>= 2.6)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 1.7)
|
unicode-display_width (>= 1.4.0, < 1.7)
|
||||||
ruby-progressbar (1.10.0)
|
rubocop-rails (2.2.0)
|
||||||
|
rack (>= 1.1)
|
||||||
|
rubocop (>= 0.72.0)
|
||||||
|
ruby-progressbar (1.10.1)
|
||||||
ruby-saml (1.9.0)
|
ruby-saml (1.9.0)
|
||||||
nokogiri (>= 1.5.10)
|
nokogiri (>= 1.5.10)
|
||||||
rufus-scheduler (3.5.2)
|
rufus-scheduler (3.5.2)
|
||||||
fugit (~> 1.1, >= 1.1.5)
|
fugit (~> 1.1, >= 1.1.5)
|
||||||
safe_yaml (1.0.4)
|
safe_yaml (1.0.5)
|
||||||
sanitize (5.0.0)
|
sanitize (5.0.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.8.0)
|
nokogiri (>= 1.8.0)
|
||||||
nokogumbo (~> 2.0)
|
nokogumbo (~> 2.0)
|
||||||
sass (3.7.4)
|
|
||||||
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.58.0)
|
|
||||||
rake (>= 0.9, < 13)
|
|
||||||
sass (~> 3.5, >= 3.5.5)
|
|
||||||
sidekiq (5.2.7)
|
sidekiq (5.2.7)
|
||||||
connection_pool (~> 2.2, >= 2.2.2)
|
connection_pool (~> 2.2, >= 2.2.2)
|
||||||
rack (>= 1.5.0)
|
rack (>= 1.5.0)
|
||||||
|
@ -578,7 +568,7 @@ GEM
|
||||||
simple_form (4.1.0)
|
simple_form (4.1.0)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
activemodel (>= 5.0)
|
activemodel (>= 5.0)
|
||||||
simplecov (0.16.1)
|
simplecov (0.17.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
json (>= 1.8, < 3)
|
json (>= 1.8, < 3)
|
||||||
simplecov-html (~> 0.10.0)
|
simplecov-html (~> 0.10.0)
|
||||||
|
@ -598,8 +588,8 @@ GEM
|
||||||
stoplight (2.1.3)
|
stoplight (2.1.3)
|
||||||
streamio-ffmpeg (3.0.2)
|
streamio-ffmpeg (3.0.2)
|
||||||
multi_json (~> 1.8)
|
multi_json (~> 1.8)
|
||||||
strong_migrations (0.3.1)
|
strong_migrations (0.4.0)
|
||||||
activerecord (>= 3.2.0)
|
activerecord (>= 5)
|
||||||
temple (0.8.1)
|
temple (0.8.1)
|
||||||
terminal-table (1.8.0)
|
terminal-table (1.8.0)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
|
@ -608,30 +598,25 @@ GEM
|
||||||
thor (0.20.3)
|
thor (0.20.3)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.9)
|
tilt (2.0.9)
|
||||||
timers (4.2.0)
|
|
||||||
tty-color (0.4.3)
|
tty-color (0.4.3)
|
||||||
tty-command (0.8.2)
|
tty-command (0.8.2)
|
||||||
pastel (~> 0.7.0)
|
pastel (~> 0.7.0)
|
||||||
tty-cursor (0.6.0)
|
tty-cursor (0.7.0)
|
||||||
tty-prompt (0.18.1)
|
tty-prompt (0.19.0)
|
||||||
necromancer (~> 0.4.0)
|
necromancer (~> 0.5.0)
|
||||||
pastel (~> 0.7.0)
|
pastel (~> 0.7.0)
|
||||||
timers (~> 4.0)
|
tty-reader (~> 0.6.0)
|
||||||
tty-cursor (~> 0.6.0)
|
tty-reader (0.6.0)
|
||||||
tty-reader (~> 0.5.0)
|
tty-cursor (~> 0.7)
|
||||||
tty-reader (0.5.0)
|
tty-screen (~> 0.7)
|
||||||
tty-cursor (~> 0.6.0)
|
|
||||||
tty-screen (~> 0.6.4)
|
|
||||||
wisper (~> 2.0.0)
|
wisper (~> 2.0.0)
|
||||||
tty-screen (0.6.5)
|
tty-screen (0.7.0)
|
||||||
twitter-text (1.14.7)
|
twitter-text (1.14.7)
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (1.2.5)
|
tzinfo (1.2.5)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
tzinfo-data (1.2019.1)
|
tzinfo-data (1.2019.2)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uglifier (4.1.20)
|
|
||||||
execjs (>= 0.3.0, < 3)
|
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.5)
|
unf_ext (0.0.7.5)
|
||||||
|
@ -639,11 +624,11 @@ GEM
|
||||||
uniform_notifier (1.12.1)
|
uniform_notifier (1.12.1)
|
||||||
warden (1.2.8)
|
warden (1.2.8)
|
||||||
rack (>= 2.0.6)
|
rack (>= 2.0.6)
|
||||||
webmock (3.5.1)
|
webmock (3.6.0)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webpacker (4.0.2)
|
webpacker (4.0.7)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
|
@ -665,7 +650,7 @@ DEPENDENCIES
|
||||||
active_record_query_trace (~> 1.6)
|
active_record_query_trace (~> 1.6)
|
||||||
addressable (~> 2.6)
|
addressable (~> 2.6)
|
||||||
annotate (~> 2.7)
|
annotate (~> 2.7)
|
||||||
aws-sdk-s3 (~> 1.39)
|
aws-sdk-s3 (~> 1.45)
|
||||||
better_errors (~> 2.5)
|
better_errors (~> 2.5)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
|
@ -678,12 +663,13 @@ DEPENDENCIES
|
||||||
capistrano-rails (~> 1.4)
|
capistrano-rails (~> 1.4)
|
||||||
capistrano-rbenv (~> 2.1)
|
capistrano-rbenv (~> 2.1)
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 3.20)
|
capybara (~> 3.25)
|
||||||
charlock_holmes (~> 0.7.6)
|
charlock_holmes (~> 0.7.6)
|
||||||
chewy (~> 5.0)
|
chewy (~> 5.0)
|
||||||
cld3 (~> 3.2.4)
|
cld3 (~> 3.2.4)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
|
connection_pool
|
||||||
derailed_benchmarks
|
derailed_benchmarks
|
||||||
devise (~> 4.6)
|
devise (~> 4.6)
|
||||||
devise-two-factor (~> 3.0)
|
devise-two-factor (~> 3.0)
|
||||||
|
@ -696,7 +682,7 @@ DEPENDENCIES
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
fog-openstack (~> 0.3)
|
fog-openstack (~> 0.3)
|
||||||
fuubar (~> 2.3)
|
fuubar (~> 2.4)
|
||||||
goldfinger (~> 2.1)
|
goldfinger (~> 2.1)
|
||||||
hamlit-rails (~> 0.2)
|
hamlit-rails (~> 0.2)
|
||||||
hiredis (~> 0.6)
|
hiredis (~> 0.6)
|
||||||
|
@ -705,7 +691,7 @@ DEPENDENCIES
|
||||||
http (~> 3.3)
|
http (~> 3.3)
|
||||||
http_accept_language (~> 2.1)
|
http_accept_language (~> 2.1)
|
||||||
http_parser.rb (~> 0.6)!
|
http_parser.rb (~> 0.6)!
|
||||||
httplog (~> 1.2)
|
httplog (~> 1.3)
|
||||||
i18n-tasks (~> 0.9)
|
i18n-tasks (~> 0.9)
|
||||||
idn-ruby
|
idn-ruby
|
||||||
iso-639
|
iso-639
|
||||||
|
@ -729,7 +715,7 @@ DEPENDENCIES
|
||||||
omniauth-cas (~> 1.1)
|
omniauth-cas (~> 1.1)
|
||||||
omniauth-saml (~> 1.10)
|
omniauth-saml (~> 1.10)
|
||||||
ostatus2 (~> 2.0)
|
ostatus2 (~> 2.0)
|
||||||
ox (~> 2.10)
|
ox (~> 2.11)
|
||||||
paperclip (~> 6.0)
|
paperclip (~> 6.0)
|
||||||
paperclip-av-transcoder (~> 0.6)
|
paperclip-av-transcoder (~> 0.6)
|
||||||
parallel_tests (~> 2.29)
|
parallel_tests (~> 2.29)
|
||||||
|
@ -757,27 +743,27 @@ DEPENDENCIES
|
||||||
rqrcode (~> 0.10)
|
rqrcode (~> 0.10)
|
||||||
rspec-rails (~> 3.8)
|
rspec-rails (~> 3.8)
|
||||||
rspec-sidekiq (~> 3.0)
|
rspec-sidekiq (~> 3.0)
|
||||||
rubocop (~> 0.69)
|
rubocop (~> 0.72)
|
||||||
|
rubocop-rails (~> 2.2)
|
||||||
sanitize (~> 5.0)
|
sanitize (~> 5.0)
|
||||||
scss_lint (~> 0.58)
|
|
||||||
sidekiq (~> 5.2)
|
sidekiq (~> 5.2)
|
||||||
sidekiq-bulk (~> 0.2.0)
|
sidekiq-bulk (~> 0.2.0)
|
||||||
sidekiq-scheduler (~> 3.0)
|
sidekiq-scheduler (~> 3.0)
|
||||||
sidekiq-unique-jobs (~> 6.0)
|
sidekiq-unique-jobs (~> 6.0)
|
||||||
simple-navigation (~> 4.0)
|
simple-navigation (~> 4.0)
|
||||||
simple_form (~> 4.1)
|
simple_form (~> 4.1)
|
||||||
simplecov (~> 0.16)
|
simplecov (~> 0.17)
|
||||||
sprockets-rails (~> 3.2)
|
sprockets-rails (~> 3.2)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 2.1.3)
|
stoplight (~> 2.1.3)
|
||||||
streamio-ffmpeg (~> 3.0)
|
streamio-ffmpeg (~> 3.0)
|
||||||
strong_migrations (~> 0.3)
|
strong_migrations (~> 0.4)
|
||||||
thor (~> 0.20)
|
thor (~> 0.20)
|
||||||
tty-command (~> 0.8)
|
tty-command (~> 0.8)
|
||||||
tty-prompt (~> 0.18)
|
tty-prompt (~> 0.19)
|
||||||
twitter-text (~> 1.14)
|
twitter-text (~> 1.14)
|
||||||
tzinfo-data (~> 1.2019)
|
tzinfo-data (~> 1.2019)
|
||||||
webmock (~> 3.5)
|
webmock (~> 3.6)
|
||||||
webpacker (~> 4.0)
|
webpacker (~> 4.0)
|
||||||
webpush
|
webpush
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ class StatusesIndex < Chewy::Index
|
||||||
field :id, type: 'long'
|
field :id, type: 'long'
|
||||||
field :account_id, type: 'long'
|
field :account_id, type: 'long'
|
||||||
|
|
||||||
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).join("\n\n") } do
|
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
|
||||||
field :stemmed, type: 'text', analyzer: 'content'
|
field :stemmed, type: 'text', analyzer: 'content'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,6 @@ class AccountsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
mark_cacheable!
|
|
||||||
|
|
||||||
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
|
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
|
||||||
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,8 +9,6 @@ class ActivityPub::CollectionsController < Api::BaseController
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
skip_session!
|
|
||||||
|
|
||||||
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
|
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
|
||||||
ActiveModelSerializers::SerializableResource.new(
|
ActiveModelSerializers::SerializableResource.new(
|
||||||
collection_presenter,
|
collection_presenter,
|
||||||
|
|
|
@ -10,10 +10,7 @@ class ActivityPub::OutboxesController < Api::BaseController
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
unless page_requested?
|
expires_in 1.minute, public: true unless page_requested?
|
||||||
skip_session!
|
|
||||||
expires_in 1.minute, public: true
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
end
|
end
|
||||||
|
|
|
@ -48,13 +48,13 @@ module Admin
|
||||||
def approve
|
def approve
|
||||||
authorize @account.user, :approve?
|
authorize @account.user, :approve?
|
||||||
@account.user.approve!
|
@account.user.approve!
|
||||||
redirect_to admin_accounts_path(pending: '1')
|
redirect_to admin_pending_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject
|
def reject
|
||||||
authorize @account.user, :reject?
|
authorize @account.user, :reject?
|
||||||
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
|
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
|
end
|
||||||
|
|
||||||
def unsilence
|
def unsilence
|
||||||
|
@ -127,6 +127,7 @@ module Admin
|
||||||
:by_domain,
|
:by_domain,
|
||||||
:active,
|
:active,
|
||||||
:pending,
|
:pending,
|
||||||
|
:disabled,
|
||||||
:silenced,
|
:silenced,
|
||||||
:suspended,
|
:suspended,
|
||||||
:username,
|
:username,
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Admin
|
||||||
authorize :domain_block, :create?
|
authorize :domain_block, :create?
|
||||||
|
|
||||||
@domain_block = DomainBlock.new(resource_params)
|
@domain_block = DomainBlock.new(resource_params)
|
||||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil
|
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
|
||||||
|
|
||||||
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
||||||
@domain_block.save
|
@domain_block.save
|
||||||
|
|
|
@ -18,7 +18,7 @@ module Admin
|
||||||
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
|
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
|
||||||
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
|
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
|
||||||
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
|
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
|
||||||
@domain_block = DomainBlock.find_by(domain: params[:id])
|
@domain_block = DomainBlock.rule_for(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::AccountActionsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_account
|
||||||
|
|
||||||
|
def create
|
||||||
|
account_action = Admin::AccountAction.new(resource_params)
|
||||||
|
account_action.target_account = @account
|
||||||
|
account_action.current_account = current_account
|
||||||
|
account_action.save!
|
||||||
|
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(
|
||||||
|
:type,
|
||||||
|
:report_id,
|
||||||
|
:warning_preset_id,
|
||||||
|
:text,
|
||||||
|
:send_email_notification
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,128 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::AccountsController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
include AccountableConcern
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
|
||||||
|
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_accounts, only: :index
|
||||||
|
before_action :set_account, except: :index
|
||||||
|
before_action :require_local_account!, only: [:enable, :approve, :reject]
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
FILTER_PARAMS = %i(
|
||||||
|
local
|
||||||
|
remote
|
||||||
|
by_domain
|
||||||
|
active
|
||||||
|
pending
|
||||||
|
disabled
|
||||||
|
silenced
|
||||||
|
suspended
|
||||||
|
username
|
||||||
|
display_name
|
||||||
|
email
|
||||||
|
ip
|
||||||
|
staff
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :account, :index?
|
||||||
|
render json: @accounts, each_serializer: REST::Admin::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @account, :show?
|
||||||
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable
|
||||||
|
authorize @account.user, :enable?
|
||||||
|
@account.user.enable!
|
||||||
|
log_action :enable, @account.user
|
||||||
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def approve
|
||||||
|
authorize @account.user, :approve?
|
||||||
|
@account.user.approve!
|
||||||
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject
|
||||||
|
authorize @account.user, :reject?
|
||||||
|
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
|
||||||
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsilence
|
||||||
|
authorize @account, :unsilence?
|
||||||
|
@account.unsilence!
|
||||||
|
log_action :unsilence, @account
|
||||||
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsuspend
|
||||||
|
authorize @account, :unsuspend?
|
||||||
|
@account.unsuspend!
|
||||||
|
log_action :unsuspend, @account
|
||||||
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_accounts
|
||||||
|
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_accounts
|
||||||
|
AccountFilter.new(filter_params).results
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.permit(*FILTER_PARAMS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@accounts.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@accounts.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@accounts.size == limit_param(LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_local_account!
|
||||||
|
forbidden unless @account.local? && @account.user.present?
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,108 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::ReportsController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
include AccountableConcern
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
|
||||||
|
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_reports, only: :index
|
||||||
|
before_action :set_report, except: :index
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
FILTER_PARAMS = %i(
|
||||||
|
resolved
|
||||||
|
account_id
|
||||||
|
target_account_id
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :report, :index?
|
||||||
|
render json: @reports, each_serializer: REST::Admin::ReportSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @report, :show?
|
||||||
|
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_to_self
|
||||||
|
authorize @report, :update?
|
||||||
|
@report.update!(assigned_account_id: current_account.id)
|
||||||
|
log_action :assigned_to_self, @report
|
||||||
|
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def unassign
|
||||||
|
authorize @report, :update?
|
||||||
|
@report.update!(assigned_account_id: nil)
|
||||||
|
log_action :unassigned, @report
|
||||||
|
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def reopen
|
||||||
|
authorize @report, :update?
|
||||||
|
@report.unresolve!
|
||||||
|
log_action :reopen, @report
|
||||||
|
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve
|
||||||
|
authorize @report, :update?
|
||||||
|
@report.resolve!(current_account)
|
||||||
|
log_action :resolve, @report
|
||||||
|
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_reports
|
||||||
|
@reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_report
|
||||||
|
@report = Report.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_reports
|
||||||
|
ReportFilter.new(filter_params).results
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.permit(*FILTER_PARAMS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@reports.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@reports.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@reports.size == limit_param(LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,7 +7,7 @@ class Api::V1::CustomEmojisController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
|
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
|
||||||
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer)
|
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false).includes(:category), each_serializer: REST::CustomEmojiSerializer)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,7 +53,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def browserable_account_notifications
|
def browserable_account_notifications
|
||||||
current_account.notifications.browserable(exclude_types)
|
current_account.notifications.browserable(exclude_types, from_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def target_statuses_from_notifications
|
def target_statuses_from_notifications
|
||||||
|
@ -90,6 +90,10 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
val
|
val
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def from_account
|
||||||
|
params[:account_id]
|
||||||
|
end
|
||||||
|
|
||||||
def pagination_params(core_params)
|
def pagination_params(core_params)
|
||||||
params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params)
|
params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,13 +1,28 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::PollsController < Api::BaseController
|
class Api::V1::PollsController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show
|
||||||
|
before_action :set_poll
|
||||||
|
before_action :refresh_poll
|
||||||
|
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def show
|
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
|
render json: @poll, serializer: REST::PollSerializer, include_results: true
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
|
|
||||||
def data_params
|
def data_params
|
||||||
return {} if params[:data].blank?
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,8 +5,8 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
|
||||||
before_action :require_user!, except: [:show, :context, :card]
|
before_action :require_user!, except: [:show, :context]
|
||||||
before_action :set_status, only: [:show, :context, :card]
|
before_action :set_status, only: [:show, :context]
|
||||||
|
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
|
@ -33,16 +33,6 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def card
|
|
||||||
@card = @status.preview_cards.first
|
|
||||||
|
|
||||||
if @card.nil?
|
|
||||||
render_empty
|
|
||||||
else
|
|
||||||
render json: @card, serializer: REST::PreviewCardSerializer
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@status = PostStatusService.new.call(current_user.account,
|
@status = PostStatusService.new.call(current_user.account,
|
||||||
text: status_params[:status],
|
text: status_params[:status],
|
||||||
|
|
|
@ -27,16 +27,18 @@ class Api::V1::Timelines::DirectController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def direct_timeline_statuses
|
def direct_timeline_statuses
|
||||||
# this query requires built in pagination.
|
account_direct_feed.get(
|
||||||
Status.as_direct_timeline(
|
|
||||||
current_account,
|
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
params[:max_id],
|
params[:max_id],
|
||||||
params[:since_id],
|
params[:since_id],
|
||||||
true # returns array of cache_ids object
|
params[:min_id]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def account_direct_feed
|
||||||
|
DirectFeed.new(current_account)
|
||||||
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
set_pagination_headers(next_path, prev_path)
|
set_pagination_headers(next_path, prev_path)
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,6 +22,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
favourite: alerts_enabled,
|
favourite: alerts_enabled,
|
||||||
reblog: alerts_enabled,
|
reblog: alerts_enabled,
|
||||||
mention: alerts_enabled,
|
mention: alerts_enabled,
|
||||||
|
poll: alerts_enabled,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +58,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def data_params
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -161,11 +161,15 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
@current_account ||= current_user.try(:account)
|
return @current_account if defined?(@current_account)
|
||||||
|
|
||||||
|
@current_account = current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_session
|
def current_session
|
||||||
@current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
|
return @current_session if defined?(@current_session)
|
||||||
|
|
||||||
|
@current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_flavour
|
def current_flavour
|
||||||
|
@ -228,11 +232,6 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_cacheable!
|
def mark_cacheable!
|
||||||
skip_session!
|
|
||||||
expires_in 0, public: true
|
expires_in 0, public: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def skip_session!
|
|
||||||
request.session_options[:skip] = true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -70,7 +70,6 @@ module AccountControllerConcern
|
||||||
|
|
||||||
def check_account_suspension
|
def check_account_suspension
|
||||||
if @account.suspended?
|
if @account.suspended?
|
||||||
skip_session!
|
|
||||||
expires_in(3.minutes, public: true)
|
expires_in(3.minutes, public: true)
|
||||||
gone
|
gone
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,13 +43,7 @@ module SignatureVerification
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
account_stoplight = Stoplight("source:#{request.ip}") { account_from_key_id(signature_params['keyId']) }
|
account = 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
|
|
||||||
|
|
||||||
if account.nil?
|
if account.nil?
|
||||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
@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?
|
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) }
|
account = stoplight_wrap_request { 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
|
|
||||||
|
|
||||||
if account.nil?
|
if account.nil?
|
||||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
@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)
|
def account_from_key_id(key_id)
|
||||||
if key_id.start_with?('acct:')
|
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)
|
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||||
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
|
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
|
account
|
||||||
end
|
end
|
||||||
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)
|
def account_refresh_key(account)
|
||||||
return if account.local? || !account.activitypub?
|
return if account.local? || !account.activitypub?
|
||||||
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
|
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class CustomCssController < ApplicationController
|
class CustomCssController < ApplicationController
|
||||||
|
skip_before_action :store_current_location
|
||||||
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
skip_session!
|
|
||||||
render plain: Setting.custom_css || '', content_type: 'text/css'
|
render plain: Setting.custom_css || '', content_type: 'text/css'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,8 +7,6 @@ class EmojisController < ApplicationController
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json do
|
format.json do
|
||||||
skip_session!
|
|
||||||
|
|
||||||
render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
|
render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
|
||||||
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
|
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,10 +20,7 @@ class FollowerAccountsController < ApplicationController
|
||||||
format.json do
|
format.json do
|
||||||
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
|
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
|
||||||
|
|
||||||
if params[:page].blank?
|
expires_in 3.minutes, public: true if params[:page].blank?
|
||||||
skip_session!
|
|
||||||
expires_in 3.minutes, public: true
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: collection_presenter,
|
render json: collection_presenter,
|
||||||
serializer: ActivityPub::CollectionSerializer,
|
serializer: ActivityPub::CollectionSerializer,
|
||||||
|
|
|
@ -20,10 +20,7 @@ class FollowingAccountsController < ApplicationController
|
||||||
format.json do
|
format.json do
|
||||||
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
|
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
|
||||||
|
|
||||||
if params[:page].blank?
|
expires_in 3.minutes, public: true if params[:page].blank?
|
||||||
skip_session!
|
|
||||||
expires_in 3.minutes, public: true
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: collection_presenter,
|
render json: collection_presenter,
|
||||||
serializer: ActivityPub::CollectionSerializer,
|
serializer: ActivityPub::CollectionSerializer,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ManifestsController < ApplicationController
|
class ManifestsController < ApplicationController
|
||||||
|
skip_before_action :store_current_location
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: InstancePresenter.new, serializer: ManifestSerializer
|
render json: InstancePresenter.new, serializer: ManifestSerializer
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,12 @@
|
||||||
class MediaController < ApplicationController
|
class MediaController < ApplicationController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
|
skip_before_action :store_current_location
|
||||||
|
|
||||||
before_action :set_media_attachment
|
before_action :set_media_attachment
|
||||||
before_action :verify_permitted_status!
|
before_action :verify_permitted_status!
|
||||||
|
before_action :check_playable, only: :player
|
||||||
|
before_action :allow_iframing, only: :player
|
||||||
|
|
||||||
content_security_policy only: :player do |p|
|
content_security_policy only: :player do |p|
|
||||||
p.frame_ancestors(false)
|
p.frame_ancestors(false)
|
||||||
|
@ -16,8 +20,6 @@ class MediaController < ApplicationController
|
||||||
|
|
||||||
def player
|
def player
|
||||||
@body_classes = 'player'
|
@body_classes = 'player'
|
||||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
|
||||||
raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -32,4 +34,12 @@ class MediaController < ApplicationController
|
||||||
# Reraise in order to get a 404 instead of a 403 error code
|
# Reraise in order to get a 404 instead of a 403 error code
|
||||||
raise ActiveRecord::RecordNotFound
|
raise ActiveRecord::RecordNotFound
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_playable
|
||||||
|
not_found unless @media_attachment.larger_media_format?
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_iframing
|
||||||
|
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
class MediaProxyController < ApplicationController
|
class MediaProxyController < ApplicationController
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
|
skip_before_action :store_current_location
|
||||||
|
|
||||||
def show
|
def show
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
if lock.acquired?
|
if lock.acquired?
|
||||||
|
|
|
@ -61,8 +61,4 @@ class Settings::IdentityProofsController < Settings::BaseController
|
||||||
def post_params
|
def post_params
|
||||||
params.require(:account_identity_proof).permit(:post_status, :status_text)
|
params.require(:account_identity_proof).permit(:post_status, :status_text)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = ''
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Settings::NotificationsController < Settings::BaseController
|
|
||||||
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 pending_account),
|
|
||||||
interactions: %i(must_be_follower must_be_following must_be_following_dm must_be_one_day_old)
|
|
||||||
)
|
|
||||||
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
|
|
@ -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
|
|
@ -8,7 +8,7 @@ class Settings::PreferencesController < Settings::BaseController
|
||||||
|
|
||||||
if current_user.update(user_params)
|
if current_user.update(user_params)
|
||||||
I18n.locale = current_user.locale
|
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
|
else
|
||||||
render :show
|
render :show
|
||||||
end
|
end
|
||||||
|
@ -16,6 +16,10 @@ class Settings::PreferencesController < Settings::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
settings_preferences_path
|
||||||
|
end
|
||||||
|
|
||||||
def user_settings
|
def user_settings
|
||||||
UserSettingsDecorator.new(current_user)
|
UserSettingsDecorator.new(current_user)
|
||||||
end
|
end
|
||||||
|
@ -46,7 +50,10 @@ class Settings::PreferencesController < Settings::BaseController
|
||||||
:setting_hide_followers_count,
|
:setting_hide_followers_count,
|
||||||
:setting_aggregate_reblogs,
|
:setting_aggregate_reblogs,
|
||||||
:setting_show_application,
|
:setting_show_application,
|
||||||
|
:setting_advanced_layout,
|
||||||
:setting_default_content_type,
|
:setting_default_content_type,
|
||||||
|
:setting_use_blurhash,
|
||||||
|
:setting_use_pending_items,
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
|
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 must_be_one_day_old)
|
interactions: %i(must_be_follower must_be_following must_be_following_dm must_be_one_day_old)
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,10 +29,7 @@ class StatusesController < ApplicationController
|
||||||
format.html do
|
format.html do
|
||||||
use_pack 'public'
|
use_pack 'public'
|
||||||
|
|
||||||
unless user_signed_in?
|
expires_in 10.seconds, public: true if current_account.nil?
|
||||||
skip_session!
|
|
||||||
expires_in 10.seconds, public: true
|
|
||||||
end
|
|
||||||
|
|
||||||
@body_classes = 'with-modals'
|
@body_classes = 'with-modals'
|
||||||
|
|
||||||
|
@ -43,8 +40,6 @@ class StatusesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
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
|
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)
|
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
|
||||||
end
|
end
|
||||||
|
@ -53,8 +48,6 @@ class StatusesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def activity
|
def activity
|
||||||
skip_session!
|
|
||||||
|
|
||||||
render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
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)
|
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
|
||||||
end
|
end
|
||||||
|
@ -64,7 +57,6 @@ class StatusesController < ApplicationController
|
||||||
use_pack 'embed'
|
use_pack 'embed'
|
||||||
raise ActiveRecord::RecordNotFound if @status.hidden?
|
raise ActiveRecord::RecordNotFound if @status.hidden?
|
||||||
|
|
||||||
skip_session!
|
|
||||||
expires_in 180, public: true
|
expires_in 180, public: true
|
||||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||||
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
|
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
|
||||||
|
@ -73,8 +65,6 @@ class StatusesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def replies
|
def replies
|
||||||
skip_session!
|
|
||||||
|
|
||||||
render json: replies_collection_presenter,
|
render json: replies_collection_presenter,
|
||||||
serializer: ActivityPub::CollectionSerializer,
|
serializer: ActivityPub::CollectionSerializer,
|
||||||
adapter: ActivityPub::Adapter,
|
adapter: ActivityPub::Adapter,
|
||||||
|
|
|
@ -17,19 +17,13 @@ class StreamEntriesController < ApplicationController
|
||||||
format.html do
|
format.html do
|
||||||
use_pack 'public'
|
use_pack 'public'
|
||||||
|
|
||||||
unless user_signed_in?
|
expires_in 5.minutes, public: true unless @stream_entry.hidden?
|
||||||
skip_session!
|
|
||||||
expires_in 5.minutes, public: true
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
|
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity)
|
||||||
end
|
end
|
||||||
|
|
||||||
format.atom do
|
format.atom do
|
||||||
unless @stream_entry.hidden?
|
expires_in 3.minutes, public: true unless @stream_entry.hidden?
|
||||||
skip_session!
|
|
||||||
expires_in 3.minutes, public: true
|
|
||||||
end
|
|
||||||
|
|
||||||
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
|
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
|
||||||
end
|
end
|
||||||
|
@ -57,7 +51,7 @@ class StreamEntriesController < ApplicationController
|
||||||
|
|
||||||
def set_stream_entry
|
def set_stream_entry
|
||||||
@stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id])
|
@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?
|
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
|
||||||
authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?
|
authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?
|
||||||
|
|
|
@ -16,24 +16,32 @@ module StreamEntriesHelper
|
||||||
if user_signed_in?
|
if user_signed_in?
|
||||||
if account.id == current_user.account_id
|
if account.id == current_user.account_id
|
||||||
link_to settings_profile_url, class: 'button logo-button' do
|
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
|
end
|
||||||
elsif current_account.following?(account) || current_account.requested?(account)
|
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
|
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
|
end
|
||||||
elsif !(account.memorial? || account.moved?)
|
elsif !(account.memorial? || account.moved?)
|
||||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } 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
|
end
|
||||||
elsif !(account.memorial? || account.moved?)
|
elsif !(account.memorial? || account.moved?)
|
||||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
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
|
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)
|
def account_badge(account, all: false)
|
||||||
if account.bot?
|
if account.bot?
|
||||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||||
|
|
|
@ -14,15 +14,15 @@ delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
|
delegate(document, '.status__content__spoiler-link', 'click', function() {
|
||||||
const contentEl = target.parentNode.parentNode.querySelector('.e-content');
|
const contentEl = this.parentNode.parentNode.querySelector('.e-content');
|
||||||
|
|
||||||
if (contentEl.style.display === 'block') {
|
if (contentEl.style.display === 'block') {
|
||||||
contentEl.style.display = 'none';
|
contentEl.style.display = 'none';
|
||||||
target.parentNode.style.marginBottom = 0;
|
this.parentNode.style.marginBottom = 0;
|
||||||
} else {
|
} else {
|
||||||
contentEl.style.display = 'block';
|
contentEl.style.display = 'block';
|
||||||
target.parentNode.style.marginBottom = null;
|
this.parentNode.style.marginBottom = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
pack:
|
pack:
|
||||||
about:
|
about:
|
||||||
admin: admin.js
|
admin: admin.js
|
||||||
auth:
|
auth: settings.js
|
||||||
common:
|
common:
|
||||||
filename: common.js
|
filename: common.js
|
||||||
stylesheet: true
|
stylesheet: true
|
||||||
|
|
|
@ -8,6 +8,7 @@ const messages = defineMessages({
|
||||||
export const ALERT_SHOW = 'ALERT_SHOW';
|
export const ALERT_SHOW = 'ALERT_SHOW';
|
||||||
export const ALERT_DISMISS = 'ALERT_DISMISS';
|
export const ALERT_DISMISS = 'ALERT_DISMISS';
|
||||||
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
||||||
|
export const ALERT_NOOP = 'ALERT_NOOP';
|
||||||
|
|
||||||
export function dismissAlert(alert) {
|
export function dismissAlert(alert) {
|
||||||
return {
|
return {
|
||||||
|
@ -36,7 +37,7 @@ export function showAlertForError(error) {
|
||||||
|
|
||||||
if (status === 404 || status === 410) {
|
if (status === 404 || status === 410) {
|
||||||
// Skip these errors as they are reflected in the UI
|
// Skip these errors as they are reflected in the UI
|
||||||
return {};
|
return { type: ALERT_NOOP };
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = statusText;
|
let message = statusText;
|
||||||
|
|
|
@ -68,6 +68,14 @@ const messages = defineMessages({
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
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) {
|
export function changeCompose(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
|
@ -81,16 +89,14 @@ export function cycleElefriendCompose() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function replyCompose(status, router) {
|
export function replyCompose(status, routerHistory) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_REPLY,
|
type: COMPOSE_REPLY,
|
||||||
status: status,
|
status: status,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (router && !getState().getIn(['compose', 'mounted'])) {
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
router.push('/statuses/new');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,29 +112,25 @@ export function resetCompose() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mentionCompose(account, router) {
|
export function mentionCompose(account, routerHistory) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_MENTION,
|
type: COMPOSE_MENTION,
|
||||||
account: account,
|
account: account,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!getState().getIn(['compose', 'mounted'])) {
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
router.push('/statuses/new');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function directCompose(account, router) {
|
export function directCompose(account, routerHistory) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_DIRECT,
|
type: COMPOSE_DIRECT,
|
||||||
account: account,
|
account: account,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!getState().getIn(['compose', 'mounted'])) {
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
router.push('/statuses/new');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -136,7 +138,8 @@ export function submitCompose(routerHistory) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
let status = getState().getIn(['compose', 'text'], '');
|
let status = getState().getIn(['compose', 'text'], '');
|
||||||
let media = getState().getIn(['compose', 'media_attachments']);
|
let media = getState().getIn(['compose', 'media_attachments']);
|
||||||
let spoilerText = getState().getIn(['compose', 'spoiler_text'], '');
|
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
|
||||||
|
let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
|
||||||
|
|
||||||
if ((!status || !status.length) && media.size === 0) {
|
if ((!status || !status.length) && media.size === 0) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import api, { getLinks } from 'flavours/glitch/util/api';
|
||||||
|
import {
|
||||||
|
importFetchedAccounts,
|
||||||
|
importFetchedStatuses,
|
||||||
|
importFetchedStatus,
|
||||||
|
} from './importer';
|
||||||
|
|
||||||
|
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
|
||||||
|
export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
|
||||||
|
|
||||||
|
export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST';
|
||||||
|
export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
|
||||||
|
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
|
||||||
|
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
|
||||||
|
|
||||||
|
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
|
||||||
|
|
||||||
|
export const mountConversations = () => ({
|
||||||
|
type: CONVERSATIONS_MOUNT,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const unmountConversations = () => ({
|
||||||
|
type: CONVERSATIONS_UNMOUNT,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const markConversationRead = conversationId => (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: CONVERSATIONS_READ,
|
||||||
|
id: conversationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/conversations/${conversationId}/read`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
|
||||||
|
dispatch(expandConversationsRequest());
|
||||||
|
|
||||||
|
const params = { max_id: maxId };
|
||||||
|
|
||||||
|
if (!maxId) {
|
||||||
|
params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoadingRecent = !!params.since_id;
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/conversations', { params })
|
||||||
|
.then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
|
||||||
|
dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
|
||||||
|
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(expandConversationsFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandConversationsRequest = () => ({
|
||||||
|
type: CONVERSATIONS_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({
|
||||||
|
type: CONVERSATIONS_FETCH_SUCCESS,
|
||||||
|
conversations,
|
||||||
|
next,
|
||||||
|
isLoadingRecent,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandConversationsFail = error => ({
|
||||||
|
type: CONVERSATIONS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateConversations = conversation => dispatch => {
|
||||||
|
dispatch(importFetchedAccounts(conversation.accounts));
|
||||||
|
|
||||||
|
if (conversation.last_status) {
|
||||||
|
dispatch(importFetchedStatus(conversation.last_status));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: CONVERSATIONS_UPDATE,
|
||||||
|
conversation,
|
||||||
|
});
|
||||||
|
};
|
|
@ -55,7 +55,7 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||||
} else {
|
} else {
|
||||||
const spoilerText = normalStatus.spoiler_text || '';
|
const spoilerText = normalStatus.spoiler_text || '';
|
||||||
const searchContent = [spoilerText, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
const emojiMap = makeEmojiMap(normalStatus);
|
const emojiMap = makeEmojiMap(normalStatus);
|
||||||
|
|
||||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
|
|
|
@ -11,7 +11,9 @@ import { saveSettings } from './settings';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { unescapeHTML } from 'flavours/glitch/util/html';
|
import { unescapeHTML } from 'flavours/glitch/util/html';
|
||||||
import { getFilters, regexFromFilters } from 'flavours/glitch/selectors';
|
import { getFiltersRegex } from 'flavours/glitch/selectors';
|
||||||
|
import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
|
||||||
|
import compareId from 'flavours/glitch/util/compare_id';
|
||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
|
|
||||||
|
@ -32,8 +34,9 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
|
||||||
|
|
||||||
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
||||||
|
|
||||||
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||||
|
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
||||||
|
|
||||||
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
||||||
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
||||||
|
@ -52,19 +55,28 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const loadPending = () => ({
|
||||||
|
type: NOTIFICATIONS_LOAD_PENDING,
|
||||||
|
});
|
||||||
|
|
||||||
export function updateNotifications(notification, intlMessages, intlLocale) {
|
export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
||||||
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||||
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||||
const filters = getFilters(getState(), { contextType: 'notifications' });
|
const filters = getFiltersRegex(getState(), { contextType: 'notifications' });
|
||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
|
||||||
if (notification.type === 'mention') {
|
if (notification.type === 'mention') {
|
||||||
const regex = regexFromFilters(filters);
|
const dropRegex = filters[0];
|
||||||
|
const regex = filters[1];
|
||||||
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
|
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
|
||||||
|
|
||||||
|
if (dropRegex && dropRegex.test(searchIndex)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
filtered = regex && regex.test(searchIndex);
|
filtered = regex && regex.test(searchIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +90,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATIONS_UPDATE,
|
type: NOTIFICATIONS_UPDATE,
|
||||||
notification,
|
notification,
|
||||||
|
usePendingItems: preferPendingItems,
|
||||||
meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
|
meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -131,10 +144,19 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
|
||||||
: excludeTypesFromFilter(activeFilter),
|
: excludeTypesFromFilter(activeFilter),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!maxId && notifications.get('items').size > 0) {
|
if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
|
||||||
params.since_id = notifications.getIn(['items', 0, 'id']);
|
const a = notifications.getIn(['pendingItems', 0, 'id']);
|
||||||
|
const b = notifications.getIn(['items', 0, 'id']);
|
||||||
|
|
||||||
|
if (a && b && compareId(a, b) > 0) {
|
||||||
|
params.since_id = a;
|
||||||
|
} else {
|
||||||
|
params.since_id = b || a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLoadingRecent = !!params.since_id;
|
||||||
|
|
||||||
dispatch(expandNotificationsRequest(isLoadingMore));
|
dispatch(expandNotificationsRequest(isLoadingMore));
|
||||||
|
|
||||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||||
|
@ -143,7 +165,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
|
||||||
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||||
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||||
|
|
||||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore));
|
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems));
|
||||||
fetchRelatedRelationships(dispatch, response.data);
|
fetchRelatedRelationships(dispatch, response.data);
|
||||||
done();
|
done();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -160,13 +182,12 @@ export function expandNotificationsRequest(isLoadingMore) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function expandNotificationsSuccess(notifications, next, isLoadingMore) {
|
export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) {
|
||||||
return {
|
return {
|
||||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||||
notifications,
|
notifications,
|
||||||
accounts: notifications.map(item => item.account),
|
|
||||||
statuses: notifications.map(item => item.status).filter(status => !!status),
|
|
||||||
next,
|
next,
|
||||||
|
usePendingItems,
|
||||||
skipLoading: !isLoadingMore,
|
skipLoading: !isLoadingMore,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,7 +48,7 @@ export function submitSearch() {
|
||||||
dispatch(importFetchedStatuses(response.data.statuses));
|
dispatch(importFetchedStatuses(response.data.statuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fetchSearchSuccess(response.data));
|
dispatch(fetchSearchSuccess(response.data, value));
|
||||||
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchSearchFail(error));
|
dispatch(fetchSearchFail(error));
|
||||||
|
@ -62,12 +62,11 @@ export function fetchSearchRequest() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fetchSearchSuccess(results) {
|
export function fetchSearchSuccess(results, searchTerm) {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_FETCH_SUCCESS,
|
type: SEARCH_FETCH_SUCCESS,
|
||||||
results,
|
results,
|
||||||
accounts: results.accounts,
|
searchTerm,
|
||||||
statuses: results.statuses,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import api from 'flavours/glitch/util/api';
|
||||||
|
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
|
import { ensureComposeIsVisible } from './compose';
|
||||||
|
|
||||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||||
|
@ -80,7 +81,7 @@ export function redraft(status, raw_text, content_type) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function deleteStatus(id, router, withRedraft = false) {
|
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
let status = getState().getIn(['statuses', id]);
|
let status = getState().getIn(['statuses', id]);
|
||||||
|
|
||||||
|
@ -97,9 +98,7 @@ export function deleteStatus(id, router, withRedraft = false) {
|
||||||
if (withRedraft) {
|
if (withRedraft) {
|
||||||
dispatch(redraft(status, response.data.text, response.data.content_type));
|
dispatch(redraft(status, response.data.text, response.data.content_type));
|
||||||
|
|
||||||
if (!getState().getIn(['compose', 'mounted'])) {
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
router.push('/statuses/new');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(deleteStatusFail(id, error));
|
dispatch(deleteStatusFail(id, error));
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
|
import { updateConversations } from './conversations';
|
||||||
import { fetchFilters } from './filters';
|
import { fetchFilters } from './filters';
|
||||||
import { getLocale } from 'mastodon/locales';
|
import { getLocale } from 'mastodon/locales';
|
||||||
|
|
||||||
|
@ -37,6 +38,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
||||||
case 'notification':
|
case 'notification':
|
||||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
||||||
break;
|
break;
|
||||||
|
case 'conversation':
|
||||||
|
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||||
|
break;
|
||||||
case 'filters_changed':
|
case 'filters_changed':
|
||||||
dispatch(fetchFilters());
|
dispatch(fetchFilters());
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import api, { getLinks } from 'flavours/glitch/util/api';
|
import api, { getLinks } from 'flavours/glitch/util/api';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
import compareId from 'flavours/glitch/util/compare_id';
|
||||||
|
import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||||
|
@ -10,10 +12,15 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
|
||||||
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
|
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
|
||||||
export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
|
export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
|
||||||
|
|
||||||
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
||||||
|
export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
|
||||||
|
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
||||||
|
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||||
|
|
||||||
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
export const loadPending = timeline => ({
|
||||||
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
type: TIMELINE_LOAD_PENDING,
|
||||||
|
timeline,
|
||||||
|
});
|
||||||
|
|
||||||
export function updateTimeline(timeline, status, accept) {
|
export function updateTimeline(timeline, status, accept) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
|
@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) {
|
||||||
type: TIMELINE_UPDATE,
|
type: TIMELINE_UPDATE,
|
||||||
timeline,
|
timeline,
|
||||||
status,
|
status,
|
||||||
|
usePendingItems: preferPendingItems,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) {
|
if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) {
|
||||||
params.since_id = timeline.getIn(['items', 0]);
|
const a = timeline.getIn(['pendingItems', 0]);
|
||||||
|
const b = timeline.getIn(['items', 0]);
|
||||||
|
|
||||||
|
if (a && b && compareId(a, b) > 0) {
|
||||||
|
params.since_id = a;
|
||||||
|
} else {
|
||||||
|
params.since_id = b || a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoadingRecent = !!params.since_id;
|
const isLoadingRecent = !!params.since_id;
|
||||||
|
@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
||||||
api(getState).get(path, { params }).then(response => {
|
api(getState).get(path, { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedStatuses(response.data));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore));
|
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
|
||||||
done();
|
done();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||||
|
@ -117,7 +132,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) {
|
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_EXPAND_SUCCESS,
|
type: TIMELINE_EXPAND_SUCCESS,
|
||||||
timeline,
|
timeline,
|
||||||
|
@ -125,6 +140,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi
|
||||||
next,
|
next,
|
||||||
partial,
|
partial,
|
||||||
isLoadingRecent,
|
isLoadingRecent,
|
||||||
|
usePendingItems,
|
||||||
skipLoading: !isLoadingMore,
|
skipLoading: !isLoadingMore,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -153,9 +169,8 @@ export function connectTimeline(timeline) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function disconnectTimeline(timeline) {
|
export const disconnectTimeline = timeline => ({
|
||||||
return {
|
type: TIMELINE_DISCONNECT,
|
||||||
type: TIMELINE_DISCONNECT,
|
timeline,
|
||||||
timeline,
|
usePendingItems: preferPendingItems,
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
searchTokens: PropTypes.list,
|
searchTokens: PropTypes.arrayOf(PropTypes.string),
|
||||||
maxLength: PropTypes.number,
|
maxLength: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -138,8 +138,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
this.setState({ suggestionsHidden: true, focused: false });
|
this.setState({ suggestionsHidden: true, focused: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocus = () => {
|
onFocus = (e) => {
|
||||||
this.setState({ focused: true });
|
this.setState({ focused: true });
|
||||||
|
if (this.props.onFocus) {
|
||||||
|
this.props.onFocus(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuggestionClick = (e) => {
|
onSuggestionClick = (e) => {
|
||||||
|
@ -189,7 +192,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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 { suggestionsHidden } = this.state;
|
||||||
const style = { direction: 'ltr' };
|
const style = { direction: 'ltr' };
|
||||||
|
|
||||||
|
@ -197,34 +200,39 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
style.direction = 'rtl';
|
style.direction = 'rtl';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return [
|
||||||
<div className='autosuggest-textarea'>
|
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||||
<label>
|
<div className='autosuggest-textarea'>
|
||||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
<label>
|
||||||
|
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
inputRef={this.setTextarea}
|
inputRef={this.setTextarea}
|
||||||
className='autosuggest-textarea__textarea'
|
className='autosuggest-textarea__textarea'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onPaste={this.onPaste}
|
onPaste={this.onPaste}
|
||||||
style={style}
|
style={style}
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
/>
|
/>
|
||||||
</label>
|
</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'}`}>
|
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||||
{suggestions.map(this.renderSuggestion)}
|
{suggestions.map(this.renderSuggestion)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
|
export default class AvatarComposite extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
accounts: ImmutablePropTypes.list.isRequired,
|
||||||
|
animate: PropTypes.bool,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
animate: autoPlayGif,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderItem (account, size, index) {
|
||||||
|
const { animate } = this.props;
|
||||||
|
|
||||||
|
let width = 50;
|
||||||
|
let height = 100;
|
||||||
|
let top = 'auto';
|
||||||
|
let left = 'auto';
|
||||||
|
let bottom = 'auto';
|
||||||
|
let right = 'auto';
|
||||||
|
|
||||||
|
if (size === 1) {
|
||||||
|
width = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size === 4 || (size === 3 && index > 0)) {
|
||||||
|
height = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size === 2) {
|
||||||
|
if (index === 0) {
|
||||||
|
right = '2px';
|
||||||
|
} else {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
} else if (size === 3) {
|
||||||
|
if (index === 0) {
|
||||||
|
right = '2px';
|
||||||
|
} else if (index > 0) {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 1) {
|
||||||
|
bottom = '2px';
|
||||||
|
} else if (index > 1) {
|
||||||
|
top = '2px';
|
||||||
|
}
|
||||||
|
} else if (size === 4) {
|
||||||
|
if (index === 0 || index === 2) {
|
||||||
|
right = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 1 || index === 3) {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 2) {
|
||||||
|
bottom = '2px';
|
||||||
|
} else {
|
||||||
|
top = '2px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
right: right,
|
||||||
|
bottom: bottom,
|
||||||
|
width: `${width}%`,
|
||||||
|
height: `${height}%`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={account.get('url')}
|
||||||
|
target='_blank'
|
||||||
|
onClick={(e) => this.props.onAccountClick(account.get('id'), e)}
|
||||||
|
title={`@${account.get('acct')}`}
|
||||||
|
key={account.get('id')}
|
||||||
|
>
|
||||||
|
<div style={style} data-avatar-of={`@${account.get('acct')}`} />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { accounts, size } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
|
||||||
|
{accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,24 +10,56 @@ export default function DisplayName ({
|
||||||
className,
|
className,
|
||||||
inline,
|
inline,
|
||||||
localDomain,
|
localDomain,
|
||||||
|
others,
|
||||||
|
onAccountClick,
|
||||||
}) {
|
}) {
|
||||||
const computedClass = classNames('display-name', { inline }, className);
|
const computedClass = classNames('display-name', { inline }, className);
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
|
let displayName, suffix;
|
||||||
|
|
||||||
let acct = account.get('acct');
|
let acct = account.get('acct');
|
||||||
|
|
||||||
if (acct.indexOf('@') === -1 && localDomain) {
|
if (acct.indexOf('@') === -1 && localDomain) {
|
||||||
acct = `${acct}@${localDomain}`;
|
acct = `${acct}@${localDomain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The result.
|
if (others && others.size > 0) {
|
||||||
return account ? (
|
displayName = others.take(2).map(a => (
|
||||||
|
<a
|
||||||
|
href={a.get('url')}
|
||||||
|
target='_blank'
|
||||||
|
onClick={(e) => onAccountClick(a.get('id'), e)}
|
||||||
|
title={`@${a.get('acct')}`}
|
||||||
|
>
|
||||||
|
<bdi key={a.get('id')}>
|
||||||
|
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} />
|
||||||
|
</bdi>
|
||||||
|
</a>
|
||||||
|
)).reduce((prev, cur) => [prev, ', ', cur]);
|
||||||
|
|
||||||
|
if (others.size - 2 > 0) {
|
||||||
|
displayName.push(` +${others.size - 2}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix = (
|
||||||
|
<a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)}>
|
||||||
|
<span className='display-name__account'>@{acct}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
|
||||||
|
suffix = <span className='display-name__account'>@{acct}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<span className={computedClass}>
|
<span className={computedClass}>
|
||||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
|
{displayName}
|
||||||
{inline ? ' ' : null}
|
{inline ? ' ' : null}
|
||||||
<span className='display-name__account'>@{acct}</span>
|
{suffix}
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props.
|
// Props.
|
||||||
|
@ -36,4 +68,6 @@ DisplayName.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
inline: PropTypes.bool,
|
inline: PropTypes.bool,
|
||||||
localDomain: PropTypes.string,
|
localDomain: PropTypes.string,
|
||||||
|
others: ImmutablePropTypes.list,
|
||||||
|
handleClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,43 +50,43 @@ export default class ErrorBoundary extends React.PureComponent {
|
||||||
<h1><FormattedMessage id='web_app_crash.title' defaultMessage="We're sorry, but something went wrong with the Mastodon app." /></h1>
|
<h1><FormattedMessage id='web_app_crash.title' defaultMessage="We're sorry, but something went wrong with the Mastodon app." /></h1>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage id='web_app_crash.content' defaultMessage='You could try any of the following:' />
|
<FormattedMessage id='web_app_crash.content' defaultMessage='You could try any of the following:' />
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<FormattedMessage
|
|
||||||
id='web_app_crash.report_issue'
|
|
||||||
defaultMessage='Report a bug in the {issuetracker}'
|
|
||||||
values={{ issuetracker: <a href='https://github.com/glitch-soc/mastodon/issues' rel='noopener' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }}
|
|
||||||
/>
|
|
||||||
{ debugInfo !== '' && (
|
|
||||||
<details>
|
|
||||||
<summary><FormattedMessage id='web_app_crash.debug_info' defaultMessage='Debug information' /></summary>
|
|
||||||
<textarea
|
|
||||||
className='web_app_crash-stacktrace'
|
|
||||||
value={debugInfo}
|
|
||||||
rows='10'
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<FormattedMessage
|
|
||||||
id='web_app_crash.reload_page'
|
|
||||||
defaultMessage='{reload} the current page'
|
|
||||||
values={{ reload: <a href='#' onClick={this.handleReload}><FormattedMessage id='web_app_crash.reload' defaultMessage='Reload' /></a> }}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
{ preferencesLink !== undefined && (
|
|
||||||
<li>
|
|
||||||
<FormattedMessage
|
|
||||||
id='web_app_crash.change_your_settings'
|
|
||||||
defaultMessage='Change your {settings}'
|
|
||||||
values={{ settings: <a href={preferencesLink}><FormattedMessage id='web_app_crash.settings' defaultMessage='settings' /></a> }}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</p>
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage
|
||||||
|
id='web_app_crash.report_issue'
|
||||||
|
defaultMessage='Report a bug in the {issuetracker}'
|
||||||
|
values={{ issuetracker: <a href='https://github.com/glitch-soc/mastodon/issues' rel='noopener' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }}
|
||||||
|
/>
|
||||||
|
{ debugInfo !== '' && (
|
||||||
|
<details>
|
||||||
|
<summary><FormattedMessage id='web_app_crash.debug_info' defaultMessage='Debug information' /></summary>
|
||||||
|
<textarea
|
||||||
|
className='web_app_crash-stacktrace'
|
||||||
|
value={debugInfo}
|
||||||
|
rows='10'
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage
|
||||||
|
id='web_app_crash.reload_page'
|
||||||
|
defaultMessage='{reload} the current page'
|
||||||
|
values={{ reload: <a href='#' onClick={this.handleReload}><FormattedMessage id='web_app_crash.reload' defaultMessage='Reload' /></a> }}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{ preferencesLink !== undefined && (
|
||||||
|
<li>
|
||||||
|
<FormattedMessage
|
||||||
|
id='web_app_crash.change_your_settings'
|
||||||
|
defaultMessage='Change your {settings}'
|
||||||
|
values={{ settings: <a href={preferencesLink}><FormattedMessage id='web_app_crash.settings' defaultMessage='settings' /></a> }}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
|
const formatNumber = num => num > 40 ? '40+' : num;
|
||||||
|
|
||||||
|
const IconWithBadge = ({ id, count, className }) => (
|
||||||
|
<i className='icon-with-badge'>
|
||||||
|
<Icon icon={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;
|
|
@ -1,10 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
|
import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
|
||||||
import getRectFromEntry from 'flavours/glitch/util/get_rect_from_entry';
|
import getRectFromEntry from 'flavours/glitch/util/get_rect_from_entry';
|
||||||
|
|
||||||
export default class IntersectionObserverArticle extends ImmutablePureComponent {
|
// Diff these props in the "unrendered" state
|
||||||
|
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
|
||||||
|
|
||||||
|
export default class IntersectionObserverArticle extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
intersectionObserverWrapper: PropTypes.object.isRequired,
|
intersectionObserverWrapper: PropTypes.object.isRequired,
|
||||||
|
@ -22,20 +24,21 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps, nextState) {
|
shouldComponentUpdate (nextProps, nextState) {
|
||||||
if (!nextState.isIntersecting && nextState.isHidden) {
|
const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
|
||||||
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
|
const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
|
||||||
// that either "isIntersecting" or "isHidden" matter, and then they're
|
if (!!isUnrendered !== !!willBeUnrendered) {
|
||||||
// the only things that matter (and updated ARIA attributes).
|
// If we're going from rendered to unrendered (or vice versa) then update
|
||||||
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
|
|
||||||
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
|
|
||||||
// If we're going from a non-intersecting state to an intersecting state,
|
|
||||||
// (i.e. offscreen to onscreen), then we definitely need to re-render
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
|
// If we are and remain hidden, diff based on props
|
||||||
return super.shouldComponentUpdate(nextProps, nextState);
|
if (isUnrendered) {
|
||||||
|
return !updateOnPropsForUnrendered.every(prop => nextProps[prop] === this.props[prop]);
|
||||||
|
}
|
||||||
|
// Else, assume the children have changed
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { intersectionObserverWrapper, id } = this.props;
|
const { intersectionObserverWrapper, id } = this.props;
|
||||||
|
|
||||||
|
@ -119,7 +122,7 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
|
||||||
data-id={id}
|
data-id={id}
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
style={style}>
|
style={style}>
|
||||||
{children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || cachedHeight) })}
|
{children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default class LoadPending extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
count: PropTypes.number,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { count } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className='load-more load-gap' onClick={this.props.onClick}>
|
||||||
|
<FormattedMessage id='load_pending' defaultMessage='{count, plural, one {# new item} other {# new items}}' values={{ count }} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import IconButton from './icon_button';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { isIOS } from 'flavours/glitch/util/is_mobile';
|
import { isIOS } from 'flavours/glitch/util/is_mobile';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state';
|
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/util/initial_state';
|
||||||
import { decode } from 'blurhash';
|
import { decode } from 'blurhash';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -101,6 +101,8 @@ class Item extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_decode () {
|
_decode () {
|
||||||
|
if (!useBlurhash) return;
|
||||||
|
|
||||||
const hash = this.props.attachment.get('blurhash');
|
const hash = this.props.attachment.get('blurhash');
|
||||||
const pixels = decode(hash, 32, 32);
|
const pixels = decode(hash, 32, 32);
|
||||||
|
|
||||||
|
@ -177,7 +179,7 @@ class Item extends React.PureComponent {
|
||||||
if (attachment.get('type') === 'unknown') {
|
if (attachment.get('type') === 'unknown') {
|
||||||
return (
|
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}%` }}>
|
<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' }}>
|
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
|
||||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -257,7 +259,6 @@ export default class MediaGallery extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
sensitive: PropTypes.bool,
|
sensitive: PropTypes.bool,
|
||||||
revealed: PropTypes.bool,
|
|
||||||
standalone: PropTypes.bool,
|
standalone: PropTypes.bool,
|
||||||
letterbox: PropTypes.bool,
|
letterbox: PropTypes.bool,
|
||||||
fullwidth: PropTypes.bool,
|
fullwidth: PropTypes.bool,
|
||||||
|
@ -268,6 +269,8 @@ export default class MediaGallery extends React.PureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
defaultWidth: PropTypes.number,
|
defaultWidth: PropTypes.number,
|
||||||
cacheWidth: PropTypes.func,
|
cacheWidth: PropTypes.func,
|
||||||
|
visible: PropTypes.bool,
|
||||||
|
onToggleVisibility: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -275,13 +278,15 @@ export default class MediaGallery extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
visible: this.props.revealed === undefined ? (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all') : this.props.revealed,
|
visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
||||||
width: this.props.defaultWidth,
|
width: this.props.defaultWidth,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (!is(nextProps.media, this.props.media) || nextProps.revealed === true) {
|
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
|
||||||
this.setState({ visible: nextProps.revealed === undefined ? (displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all') : nextProps.revealed });
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,7 +299,11 @@ export default class MediaGallery extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpen = () => {
|
handleOpen = () => {
|
||||||
this.setState({ visible: !this.state.visible });
|
if (this.props.onToggleVisibility) {
|
||||||
|
this.props.onToggleVisibility();
|
||||||
|
} else {
|
||||||
|
this.setState({ visible: !this.state.visible });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick = (index) => {
|
handleClick = (index) => {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
|
import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
|
||||||
import LoadMore from './load_more';
|
import LoadMore from './load_more';
|
||||||
|
import LoadPending from './load_pending';
|
||||||
import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper';
|
import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
scrollKey: PropTypes.string.isRequired,
|
scrollKey: PropTypes.string.isRequired,
|
||||||
onLoadMore: PropTypes.func,
|
onLoadMore: PropTypes.func,
|
||||||
|
onLoadPending: PropTypes.func,
|
||||||
onScrollToTop: PropTypes.func,
|
onScrollToTop: PropTypes.func,
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
trackScroll: PropTypes.bool,
|
trackScroll: PropTypes.bool,
|
||||||
|
@ -28,6 +30,7 @@ export default class ScrollableList extends PureComponent {
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
showLoading: PropTypes.bool,
|
showLoading: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
|
numPending: PropTypes.number,
|
||||||
prepend: PropTypes.node,
|
prepend: PropTypes.node,
|
||||||
alwaysPrepend: PropTypes.bool,
|
alwaysPrepend: PropTypes.bool,
|
||||||
emptyMessage: PropTypes.node,
|
emptyMessage: PropTypes.node,
|
||||||
|
@ -222,12 +225,18 @@ export default class ScrollableList extends PureComponent {
|
||||||
return !(location.state && location.state.mastodonModalOpen);
|
return !(location.state && location.state.mastodonModalOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLoadPending = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onLoadPending();
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
|
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
|
||||||
const { fullscreen } = this.state;
|
const { fullscreen } = this.state;
|
||||||
const childrenCount = React.Children.count(children);
|
const childrenCount = React.Children.count(children);
|
||||||
|
|
||||||
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||||
|
const loadPending = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null;
|
||||||
let scrollableArea = null;
|
let scrollableArea = null;
|
||||||
|
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
|
@ -248,6 +257,8 @@ export default class ScrollableList extends PureComponent {
|
||||||
<div role='feed' className='item-list'>
|
<div role='feed' className='item-list'>
|
||||||
{prepend}
|
{prepend}
|
||||||
|
|
||||||
|
{loadPending}
|
||||||
|
|
||||||
{React.Children.map(this.props.children, (child, index) => (
|
{React.Children.map(this.props.children, (child, index) => (
|
||||||
<IntersectionObserverArticleContainer
|
<IntersectionObserverArticleContainer
|
||||||
key={child.key}
|
key={child.key}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
export default
|
||||||
|
class Spoilers extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
spoilerText: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
hidden: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSpoilerClick = () => {
|
||||||
|
this.setState({ hidden: !this.state.hidden });
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { spoilerText, children } = this.props;
|
||||||
|
const { hidden } = this.state;
|
||||||
|
|
||||||
|
const toggleText = hidden ?
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.show_more'
|
||||||
|
defaultMessage='Show more'
|
||||||
|
key='0'
|
||||||
|
/> :
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.show_less'
|
||||||
|
defaultMessage='Show less'
|
||||||
|
key='0'
|
||||||
|
/>;
|
||||||
|
|
||||||
|
return ([
|
||||||
|
<p className='spoiler__text'>
|
||||||
|
{spoilerText}
|
||||||
|
{' '}
|
||||||
|
<button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>
|
||||||
|
{toggleText}
|
||||||
|
</button>
|
||||||
|
</p>,
|
||||||
|
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import NotificationOverlayContainer from 'flavours/glitch/features/notifications
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
|
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
|
||||||
import PollContainer from 'flavours/glitch/containers/poll_container';
|
import PollContainer from 'flavours/glitch/containers/poll_container';
|
||||||
|
import { displayMedia } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
// We use the component (and not the container) since we do not want
|
// We use the component (and not the container) since we do not want
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
|
@ -38,8 +39,24 @@ export const textForScreenReader = (intl, status, rebloggedByText = false, expan
|
||||||
return values.join(', ');
|
return values.join(', ');
|
||||||
};
|
};
|
||||||
|
|
||||||
@injectIntl
|
export const defaultMediaVisibility = (status, settings) => {
|
||||||
export default class Status extends ImmutablePureComponent {
|
if (!status) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||||
|
status = status.get('reblog');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
|
@ -49,6 +66,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
containerId: PropTypes.string,
|
containerId: PropTypes.string,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
|
otherAccounts: ImmutablePropTypes.list,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
|
@ -66,6 +84,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
collapse: PropTypes.bool,
|
collapse: PropTypes.bool,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
unread: PropTypes.bool,
|
||||||
prepend: PropTypes.string,
|
prepend: PropTypes.string,
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
onMoveUp: PropTypes.func,
|
onMoveUp: PropTypes.func,
|
||||||
|
@ -76,12 +95,18 @@ export default class Status extends ImmutablePureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
cacheMediaWidth: PropTypes.func,
|
cacheMediaWidth: PropTypes.func,
|
||||||
cachedMediaWidth: PropTypes.number,
|
cachedMediaWidth: PropTypes.number,
|
||||||
|
onClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
isCollapsed: false,
|
isCollapsed: false,
|
||||||
autoCollapsed: false,
|
autoCollapsed: false,
|
||||||
isExpanded: undefined,
|
isExpanded: undefined,
|
||||||
|
showMedia: undefined,
|
||||||
|
statusId: undefined,
|
||||||
|
revealBehindCW: undefined,
|
||||||
|
showCard: false,
|
||||||
|
forceFilter: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
|
@ -91,8 +116,6 @@ export default class Status extends ImmutablePureComponent {
|
||||||
'account',
|
'account',
|
||||||
'settings',
|
'settings',
|
||||||
'prepend',
|
'prepend',
|
||||||
'boostModal',
|
|
||||||
'favouriteModal',
|
|
||||||
'muted',
|
'muted',
|
||||||
'collapse',
|
'collapse',
|
||||||
'notification',
|
'notification',
|
||||||
|
@ -103,6 +126,8 @@ export default class Status extends ImmutablePureComponent {
|
||||||
updateOnStates = [
|
updateOnStates = [
|
||||||
'isExpanded',
|
'isExpanded',
|
||||||
'isCollapsed',
|
'isCollapsed',
|
||||||
|
'showMedia',
|
||||||
|
'forceFilter',
|
||||||
]
|
]
|
||||||
|
|
||||||
// If our settings have changed to disable collapsed statuses, then we
|
// If our settings have changed to disable collapsed statuses, then we
|
||||||
|
@ -160,6 +185,20 @@ export default class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
||||||
|
update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
|
||||||
|
update.statusId = nextProps.status.get('id');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) {
|
||||||
|
update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
|
||||||
|
if (update.revealBehindCW) {
|
||||||
|
update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
|
||||||
|
}
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
return updated ? update : null;
|
return updated ? update : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,28 +258,32 @@ export default class Status extends ImmutablePureComponent {
|
||||||
this.setState({ autoCollapsed: true });
|
this.setState({ autoCollapsed: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards');
|
// Hack to fix timeline jumps when a preview card is fetched
|
||||||
|
this.setState({
|
||||||
|
showCard: !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hack to fix timeline jumps on second rendering when auto-collapsing
|
||||||
|
// or on subsequent rendering when a preview card has been fetched
|
||||||
getSnapshotBeforeUpdate (prevProps, prevState) {
|
getSnapshotBeforeUpdate (prevProps, prevState) {
|
||||||
if (this.props.getScrollPosition) {
|
if (!this.props.getScrollPosition) return null;
|
||||||
|
|
||||||
|
const { muted, hidden, status, settings } = this.props;
|
||||||
|
|
||||||
|
const doShowCard = !muted && !hidden && status && status.get('card') && settings.get('inline_preview_cards');
|
||||||
|
if (this.state.autoCollapsed || (doShowCard && !this.state.showCard)) {
|
||||||
|
if (doShowCard) this.setState({ showCard: true });
|
||||||
|
if (this.state.autoCollapsed) this.setState({ autoCollapsed: false });
|
||||||
return this.props.getScrollPosition();
|
return this.props.getScrollPosition();
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hack to fix timeline jumps on second rendering when auto-collapsing
|
|
||||||
componentDidUpdate (prevProps, prevState, snapshot) {
|
componentDidUpdate (prevProps, prevState, snapshot) {
|
||||||
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards');
|
if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) {
|
||||||
if (this.state.autoCollapsed || (doShowCard && !this.didShowCard)) {
|
this.props.updateScrollBottom(snapshot.height - snapshot.top);
|
||||||
if (doShowCard) this.didShowCard = true;
|
|
||||||
if (this.state.autoCollapsed) this.setState({ autoCollapsed: false });
|
|
||||||
if (snapshot !== null && this.props.updateScrollBottom) {
|
|
||||||
if (this.node.offsetTop < snapshot.top) {
|
|
||||||
this.props.updateScrollBottom(snapshot.height - snapshot.top);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,17 +329,21 @@ export default class Status extends ImmutablePureComponent {
|
||||||
const { status } = this.props;
|
const { status } = this.props;
|
||||||
const { isCollapsed } = this.state;
|
const { isCollapsed } = this.state;
|
||||||
if (!router) return;
|
if (!router) return;
|
||||||
if (destination === undefined) {
|
|
||||||
destination = `/statuses/${
|
|
||||||
status.getIn(['reblog', 'id'], status.get('id'))
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
|
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
|
||||||
if (isCollapsed) this.setCollapsed(false);
|
if (isCollapsed) this.setCollapsed(false);
|
||||||
else if (e.shiftKey) {
|
else if (e.shiftKey) {
|
||||||
this.setCollapsed(true);
|
this.setCollapsed(true);
|
||||||
document.getSelection().removeAllRanges();
|
document.getSelection().removeAllRanges();
|
||||||
|
} else if (this.props.onClick) {
|
||||||
|
this.props.onClick();
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
if (destination === undefined) {
|
||||||
|
destination = `/statuses/${
|
||||||
|
status.getIn(['reblog', 'id'], status.get('id'))
|
||||||
|
}`;
|
||||||
|
}
|
||||||
let state = {...router.history.location.state};
|
let state = {...router.history.location.state};
|
||||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||||
router.history.push(destination, state);
|
router.history.push(destination, state);
|
||||||
|
@ -305,6 +352,10 @@ export default class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleToggleMediaVisibility = () => {
|
||||||
|
this.setState({ showMedia: !this.state.showMedia });
|
||||||
|
}
|
||||||
|
|
||||||
handleAccountClick = (e) => {
|
handleAccountClick = (e) => {
|
||||||
if (this.context.router && e.button === 0) {
|
if (this.context.router && e.button === 0) {
|
||||||
const id = e.currentTarget.getAttribute('data-id');
|
const id = e.currentTarget.getAttribute('data-id');
|
||||||
|
@ -374,6 +425,18 @@ export default class Status extends ImmutablePureComponent {
|
||||||
this.setCollapsed(!this.state.isCollapsed);
|
this.setCollapsed(!this.state.isCollapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleHotkeyToggleSensitive = () => {
|
||||||
|
this.handleToggleMediaVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUnfilterClick = e => {
|
||||||
|
const { onUnfilter, status } = this.props;
|
||||||
|
onUnfilter(status.get('reblog') ? status.get('reblog') : status, () => this.setState({ forceFilter: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilterClick = () => {
|
||||||
|
this.setState({ forceFilter: true });
|
||||||
|
}
|
||||||
|
|
||||||
handleRef = c => {
|
handleRef = c => {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
|
@ -399,6 +462,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
intl,
|
intl,
|
||||||
status,
|
status,
|
||||||
account,
|
account,
|
||||||
|
otherAccounts,
|
||||||
settings,
|
settings,
|
||||||
collapsed,
|
collapsed,
|
||||||
muted,
|
muted,
|
||||||
|
@ -408,6 +472,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
onOpenMedia,
|
onOpenMedia,
|
||||||
notification,
|
notification,
|
||||||
hidden,
|
hidden,
|
||||||
|
unread,
|
||||||
featured,
|
featured,
|
||||||
...other
|
...other
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -431,7 +496,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
|
if ((status.get('filtered') || status.getIn(['reblog', 'filtered'])) && (this.state.forceFilter === true || settings.get('filtering_behavior') !== 'content_warning')) {
|
||||||
const minHandlers = this.props.muted ? {} : {
|
const minHandlers = this.props.muted ? {} : {
|
||||||
moveUp: this.handleHotkeyMoveUp,
|
moveUp: this.handleHotkeyMoveUp,
|
||||||
moveDown: this.handleHotkeyMoveDown,
|
moveDown: this.handleHotkeyMoveDown,
|
||||||
|
@ -441,6 +506,12 @@ export default class Status extends ImmutablePureComponent {
|
||||||
<HotKeys handlers={minHandlers}>
|
<HotKeys handlers={minHandlers}>
|
||||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
|
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
|
||||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||||
|
{settings.get('filtering_behavior') !== 'upstream' && ' '}
|
||||||
|
{settings.get('filtering_behavior') !== 'upstream' && (
|
||||||
|
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
|
||||||
|
<FormattedMessage id='status.show_filter_reason' defaultMessage='(show why)' />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
@ -472,16 +543,16 @@ export default class Status extends ImmutablePureComponent {
|
||||||
media={status.get('media_attachments')}
|
media={status.get('media_attachments')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attachments.getIn([0, 'type']) === 'video') { // Media type is 'video'
|
} else if (['video', 'audio'].includes(attachments.getIn([0, 'type']))) {
|
||||||
const video = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||||
{Component => (<Component
|
{Component => (<Component
|
||||||
preview={video.get('preview_url')}
|
preview={attachment.get('preview_url')}
|
||||||
blurhash={video.get('blurhash')}
|
blurhash={attachment.get('blurhash')}
|
||||||
src={video.get('url')}
|
src={attachment.get('url')}
|
||||||
alt={video.get('description')}
|
alt={attachment.get('description')}
|
||||||
inline
|
inline
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
letterbox={settings.getIn(['media', 'letterbox'])}
|
||||||
|
@ -490,11 +561,12 @@ export default class Status extends ImmutablePureComponent {
|
||||||
onOpenVideo={this.handleOpenVideo}
|
onOpenVideo={this.handleOpenVideo}
|
||||||
width={this.props.cachedMediaWidth}
|
width={this.props.cachedMediaWidth}
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
revealed={settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text') ? true : undefined}
|
visible={this.state.showMedia}
|
||||||
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
/>)}
|
/>)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
);
|
);
|
||||||
mediaIcon = 'video-camera';
|
mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music';
|
||||||
} else { // Media type is 'image' or 'gifv'
|
} else { // Media type is 'image' or 'gifv'
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||||
|
@ -508,7 +580,8 @@ export default class Status extends ImmutablePureComponent {
|
||||||
onOpenMedia={this.props.onOpenMedia}
|
onOpenMedia={this.props.onOpenMedia}
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
defaultWidth={this.props.cachedMediaWidth}
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
revealed={settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text') ? true : undefined}
|
visible={this.state.showMedia}
|
||||||
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
|
@ -566,12 +639,14 @@ export default class Status extends ImmutablePureComponent {
|
||||||
toggleSpoiler: this.handleExpandedToggle,
|
toggleSpoiler: this.handleExpandedToggle,
|
||||||
bookmark: this.handleHotkeyBookmark,
|
bookmark: this.handleHotkeyBookmark,
|
||||||
toggleCollapse: this.handleHotkeyCollapse,
|
toggleCollapse: this.handleHotkeyCollapse,
|
||||||
|
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||||
};
|
};
|
||||||
|
|
||||||
const computedClass = classNames('status', `status-${status.get('visibility')}`, {
|
const computedClass = classNames('status', `status-${status.get('visibility')}`, {
|
||||||
collapsed: isCollapsed,
|
collapsed: isCollapsed,
|
||||||
'has-background': isCollapsed && background,
|
'has-background': isCollapsed && background,
|
||||||
'status__wrapper-reply': !!status.get('in_reply_to_id'),
|
'status__wrapper-reply': !!status.get('in_reply_to_id'),
|
||||||
|
read: unread === false,
|
||||||
muted,
|
muted,
|
||||||
}, 'focusable');
|
}, 'focusable');
|
||||||
|
|
||||||
|
@ -602,6 +677,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
friend={account}
|
friend={account}
|
||||||
collapsed={isCollapsed}
|
collapsed={isCollapsed}
|
||||||
parseClick={parseClick}
|
parseClick={parseClick}
|
||||||
|
otherAccounts={otherAccounts}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
|
@ -611,6 +687,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
collapsible={settings.getIn(['collapsed', 'enabled'])}
|
collapsible={settings.getIn(['collapsed', 'enabled'])}
|
||||||
collapsed={isCollapsed}
|
collapsed={isCollapsed}
|
||||||
setCollapsed={setCollapsed}
|
setCollapsed={setCollapsed}
|
||||||
|
directMessage={!!otherAccounts}
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<StatusContent
|
<StatusContent
|
||||||
|
@ -628,6 +705,8 @@ export default class Status extends ImmutablePureComponent {
|
||||||
status={status}
|
status={status}
|
||||||
account={status.get('account')}
|
account={status.get('account')}
|
||||||
showReplyCount={settings.get('show_reply_count')}
|
showReplyCount={settings.get('show_reply_count')}
|
||||||
|
directMessage={!!otherAccounts}
|
||||||
|
onFilter={this.handleFilterClick}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{notification ? (
|
{notification ? (
|
||||||
|
|
|
@ -35,6 +35,7 @@ const messages = defineMessages({
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||||
|
hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const obfuscatedCount = count => {
|
const obfuscatedCount = count => {
|
||||||
|
@ -69,8 +70,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
onMuteConversation: PropTypes.func,
|
onMuteConversation: PropTypes.func,
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
onBookmark: PropTypes.func,
|
onBookmark: PropTypes.func,
|
||||||
|
onFilter: PropTypes.func,
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
showReplyCount: PropTypes.bool,
|
showReplyCount: PropTypes.bool,
|
||||||
|
directMessage: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -151,8 +154,12 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleOpen = () => {
|
handleOpen = () => {
|
||||||
let state = {...this.context.router.history.location.state};
|
let state = {...this.context.router.history.location.state};
|
||||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
if (state.mastodonModalOpen) {
|
||||||
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`, state);
|
this.context.router.history.replace(`/statuses/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 });
|
||||||
|
} else {
|
||||||
|
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||||
|
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`, state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEmbed = () => {
|
handleEmbed = () => {
|
||||||
|
@ -186,8 +193,12 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFilterClick = () => {
|
||||||
|
this.props.onFilter();
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl, withDismiss, showReplyCount } = this.props;
|
const { status, intl, withDismiss, showReplyCount, directMessage } = this.props;
|
||||||
|
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
|
@ -258,6 +269,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filterButton = status.get('filtered') && (
|
||||||
|
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleFilterClick} />
|
||||||
|
);
|
||||||
|
|
||||||
let replyButton = (
|
let replyButton = (
|
||||||
<IconButton
|
<IconButton
|
||||||
className='status__action-bar-button'
|
className='status__action-bar-button'
|
||||||
|
@ -278,14 +293,16 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
{replyButton}
|
{replyButton}
|
||||||
<IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
{!directMessage && [
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
<IconButton key='reblog-button' className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} />,
|
||||||
{shareButton}
|
<IconButton key='favourite-button' className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />,
|
||||||
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
shareButton,
|
||||||
|
<IconButton key='bookmark-button' className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />,
|
||||||
<div className='status__action-bar-dropdown'>
|
filterButton,
|
||||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
|
<div key='dropdown-button' className='status__action-bar-dropdown'>
|
||||||
</div>
|
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
|
||||||
|
</div>,
|
||||||
|
]}
|
||||||
|
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
// Mastodon imports.
|
// Mastodon imports.
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
import AvatarOverlay from './avatar_overlay';
|
import AvatarOverlay from './avatar_overlay';
|
||||||
|
import AvatarComposite from './avatar_composite';
|
||||||
import DisplayName from './display_name';
|
import DisplayName from './display_name';
|
||||||
|
|
||||||
export default class StatusHeader extends React.PureComponent {
|
export default class StatusHeader extends React.PureComponent {
|
||||||
|
@ -14,12 +15,18 @@ export default class StatusHeader extends React.PureComponent {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
friend: ImmutablePropTypes.map,
|
friend: ImmutablePropTypes.map,
|
||||||
parseClick: PropTypes.func.isRequired,
|
parseClick: PropTypes.func.isRequired,
|
||||||
|
otherAccounts: ImmutablePropTypes.list,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles clicks on account name/image
|
// Handles clicks on account name/image
|
||||||
|
handleClick = (id, e) => {
|
||||||
|
const { parseClick } = this.props;
|
||||||
|
parseClick(e, `/accounts/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
handleAccountClick = (e) => {
|
handleAccountClick = (e) => {
|
||||||
const { status, parseClick } = this.props;
|
const { status } = this.props;
|
||||||
parseClick(e, `/accounts/${status.getIn(['account', 'id'])}`);
|
this.handleClick(status.getIn(['account', 'id']), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rendering.
|
// Rendering.
|
||||||
|
@ -27,36 +34,55 @@ export default class StatusHeader extends React.PureComponent {
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
friend,
|
friend,
|
||||||
|
otherAccounts,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
|
||||||
return (
|
let statusAvatar;
|
||||||
<div className='status__info__account' >
|
if (otherAccounts && otherAccounts.size > 0) {
|
||||||
<a
|
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} onAccountClick={this.handleClick} />;
|
||||||
href={account.get('url')}
|
} else if (friend === undefined || friend === null) {
|
||||||
target='_blank'
|
statusAvatar = <Avatar account={account} size={48} />;
|
||||||
className='status__avatar'
|
} else {
|
||||||
onClick={this.handleAccountClick}
|
statusAvatar = <AvatarOverlay account={account} friend={friend} />;
|
||||||
>
|
}
|
||||||
{
|
|
||||||
friend ? (
|
if (!otherAccounts) {
|
||||||
<AvatarOverlay account={account} friend={friend} />
|
return (
|
||||||
) : (
|
<div className='status__info__account'>
|
||||||
<Avatar account={account} size={48} />
|
<a
|
||||||
)
|
href={account.get('url')}
|
||||||
}
|
target='_blank'
|
||||||
</a>
|
className='status__avatar'
|
||||||
<a
|
onClick={this.handleAccountClick}
|
||||||
href={account.get('url')}
|
>
|
||||||
target='_blank'
|
{statusAvatar}
|
||||||
className='status__display-name'
|
</a>
|
||||||
onClick={this.handleAccountClick}
|
<a
|
||||||
>
|
href={account.get('url')}
|
||||||
<DisplayName account={account} />
|
target='_blank'
|
||||||
</a>
|
className='status__display-name'
|
||||||
</div>
|
onClick={this.handleAccountClick}
|
||||||
);
|
>
|
||||||
|
<DisplayName account={account} others={otherAccounts} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// This is a DM conversation
|
||||||
|
return (
|
||||||
|
<div className='status__info__account'>
|
||||||
|
<span className='status__avatar'>
|
||||||
|
{statusAvatar}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className='status__display-name'>
|
||||||
|
<DisplayName account={account} others={otherAccounts} onAccountClick={this.handleClick} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,13 @@ import VisibilityIcon from './status_visibility_icon';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
|
collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
|
||||||
uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
|
uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
|
||||||
|
inReplyTo: { id: 'status.in_reply_to', defaultMessage: 'This toot is a reply' },
|
||||||
|
previewCard: { id: 'status.has_preview_card', defaultMessage: 'Features an attached preview card' },
|
||||||
|
pictures: { id: 'status.has_pictures', defaultMessage: 'Features attached pictures' },
|
||||||
|
poll: { id: 'status.is_poll', defaultMessage: 'This toot is a poll' },
|
||||||
|
video: { id: 'status.has_video', defaultMessage: 'Features attached videos' },
|
||||||
|
audio: { id: 'status.has_audio', defaultMessage: 'Features attached audio files' },
|
||||||
|
localOnly: { id: 'status.local_only', defaultMessage: 'Only visible from your instance' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@injectIntl
|
@injectIntl
|
||||||
|
@ -22,6 +29,7 @@ export default class StatusIcons extends React.PureComponent {
|
||||||
mediaIcon: PropTypes.string,
|
mediaIcon: PropTypes.string,
|
||||||
collapsible: PropTypes.bool,
|
collapsible: PropTypes.bool,
|
||||||
collapsed: PropTypes.bool,
|
collapsed: PropTypes.bool,
|
||||||
|
directMessage: PropTypes.bool,
|
||||||
setCollapsed: PropTypes.func.isRequired,
|
setCollapsed: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -35,6 +43,23 @@ export default class StatusIcons extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mediaIconTitleText () {
|
||||||
|
const { intl, mediaIcon } = this.props;
|
||||||
|
|
||||||
|
switch (mediaIcon) {
|
||||||
|
case 'link':
|
||||||
|
return intl.formatMessages(message.previewCard);
|
||||||
|
case 'picture-o':
|
||||||
|
return intl.formatMessage(messages.pictures);
|
||||||
|
case 'tasks':
|
||||||
|
return intl.formatMessage(messages.poll);
|
||||||
|
case 'video-camera':
|
||||||
|
return intl.formatMessage(messages.video);
|
||||||
|
case 'music':
|
||||||
|
return intl.formatMessage(messages.audio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rendering.
|
// Rendering.
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
|
@ -42,6 +67,7 @@ export default class StatusIcons extends React.PureComponent {
|
||||||
mediaIcon,
|
mediaIcon,
|
||||||
collapsible,
|
collapsible,
|
||||||
collapsed,
|
collapsed,
|
||||||
|
directMessage,
|
||||||
intl,
|
intl,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -51,17 +77,23 @@ export default class StatusIcons extends React.PureComponent {
|
||||||
<i
|
<i
|
||||||
className={`fa fa-fw fa-comment status__reply-icon`}
|
className={`fa fa-fw fa-comment status__reply-icon`}
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
|
title={intl.formatMessage(messages.inReplyTo)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{status.get('local_only') &&
|
||||||
|
<i
|
||||||
|
className={`fa fa-fw fa-home`}
|
||||||
|
aria-hidden='true'
|
||||||
|
title={intl.formatMessage(messages.localOnly)}
|
||||||
|
/>}
|
||||||
{mediaIcon ? (
|
{mediaIcon ? (
|
||||||
<i
|
<i
|
||||||
className={`fa fa-fw fa-${mediaIcon} status__media-icon`}
|
className={`fa fa-fw fa-${mediaIcon} status__media-icon`}
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
|
title={this.mediaIconTitleText()}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{(
|
{!directMessage && <VisibilityIcon visibility={status.get('visibility')} />}
|
||||||
<VisibilityIcon visibility={status.get('visibility')} />
|
|
||||||
)}
|
|
||||||
{collapsible ? (
|
{collapsible ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
className='status__collapse-button'
|
className='status__collapse-button'
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Status from 'flavours/glitch/components/status';
|
import Status from 'flavours/glitch/components/status';
|
||||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
import { makeGetStatus, regexFromFilters, toServerSideType } from 'flavours/glitch/selectors';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
|
@ -25,7 +26,11 @@ import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { filterEditLink } from 'flavours/glitch/util/backend_links';
|
||||||
import { showAlertForError } from '../actions/alerts';
|
import { showAlertForError } from '../actions/alerts';
|
||||||
|
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||||
|
import Spoilers from '../components/spoilers';
|
||||||
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
|
@ -36,6 +41,10 @@ const messages = defineMessages({
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||||
|
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
|
||||||
|
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
|
||||||
|
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
|
||||||
|
editFilter: { id: 'confirmations.unfilter.edit_filter', defaultMessage: 'Edit filter' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
|
@ -69,7 +78,7 @@ const makeMapStateToProps = () => {
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
|
|
||||||
onReply (status, router) {
|
onReply (status, router) {
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
|
@ -96,11 +105,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
onReblog (status, e) {
|
onReblog (status, e) {
|
||||||
if (e.shiftKey || !boostModal) {
|
dispatch((_, getState) => {
|
||||||
this.onModalReblog(status);
|
let state = getState();
|
||||||
} else {
|
if (state.getIn(['local_settings', 'confirm_boost_missing_media_description']) && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
|
||||||
}
|
} else if (e.shiftKey || !boostModal) {
|
||||||
|
this.onModalReblog(status);
|
||||||
|
} else {
|
||||||
|
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onBookmark (status) {
|
onBookmark (status) {
|
||||||
|
@ -184,6 +198,48 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onUnfilter (status, onConfirm) {
|
||||||
|
dispatch((_, getState) => {
|
||||||
|
let state = getState();
|
||||||
|
const serverSideType = toServerSideType(contextType);
|
||||||
|
const enabledFilters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))).toArray();
|
||||||
|
const searchIndex = status.get('search_index');
|
||||||
|
const matchingFilters = enabledFilters.filter(filter => regexFromFilters([filter]).test(searchIndex));
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: [
|
||||||
|
<FormattedMessage id='confirmations.unfilter' defaultMessage='Information about this filtered toot' />,
|
||||||
|
<div className='filtered-status-info'>
|
||||||
|
<Spoilers spoilerText={intl.formatMessage(messages.author)}>
|
||||||
|
<AccountContainer id={status.getIn(['account', 'id'])} />
|
||||||
|
</Spoilers>
|
||||||
|
<Spoilers spoilerText={intl.formatMessage(messages.matchingFilters, {count: matchingFilters.size})}>
|
||||||
|
<ul>
|
||||||
|
{matchingFilters.map(filter => (
|
||||||
|
<li>
|
||||||
|
{filter.get('phrase')}
|
||||||
|
{!!filterEditLink && ' '}
|
||||||
|
{!!filterEditLink && (
|
||||||
|
<a
|
||||||
|
target='_blank'
|
||||||
|
className='filtered-status-edit-link'
|
||||||
|
title={intl.formatMessage(messages.editFilter)}
|
||||||
|
href={filterEditLink(filter.get('id'))}
|
||||||
|
>
|
||||||
|
<Icon icon='pencil' />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Spoilers>
|
||||||
|
</div>
|
||||||
|
],
|
||||||
|
confirm: intl.formatMessage(messages.unfilterConfirm),
|
||||||
|
onConfirm: onConfirm,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onReport (status) {
|
onReport (status) {
|
||||||
dispatch(initReport(status.get('account'), status));
|
dispatch(initReport(status.get('account'), status));
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
|
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||||
|
|
|
@ -55,6 +55,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
onPickEmoji: PropTypes.func,
|
onPickEmoji: PropTypes.func,
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
anyMedia: PropTypes.bool,
|
anyMedia: PropTypes.bool,
|
||||||
|
singleColumn: PropTypes.bool,
|
||||||
|
|
||||||
advancedOptions: ImmutablePropTypes.map,
|
advancedOptions: ImmutablePropTypes.map,
|
||||||
layout: PropTypes.string,
|
layout: PropTypes.string,
|
||||||
|
@ -66,8 +67,6 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
preselectOnReply: PropTypes.bool,
|
preselectOnReply: PropTypes.bool,
|
||||||
onChangeSpoilerness: PropTypes.func,
|
onChangeSpoilerness: PropTypes.func,
|
||||||
onChangeVisibility: PropTypes.func,
|
onChangeVisibility: PropTypes.func,
|
||||||
onMount: PropTypes.func,
|
|
||||||
onUnmount: PropTypes.func,
|
|
||||||
onPaste: PropTypes.func,
|
onPaste: PropTypes.func,
|
||||||
onMediaDescriptionConfirm: PropTypes.func,
|
onMediaDescriptionConfirm: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
@ -141,6 +140,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.composeForm = c;
|
||||||
|
};
|
||||||
|
|
||||||
// Inserts an emoji at the caret.
|
// Inserts an emoji at the caret.
|
||||||
handleEmoji = (data) => {
|
handleEmoji = (data) => {
|
||||||
const { textarea: { selectionStart } } = this;
|
const { textarea: { selectionStart } } = this;
|
||||||
|
@ -192,19 +195,12 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tells our state the composer has been mounted.
|
handleFocus = () => {
|
||||||
componentDidMount () {
|
if (this.composeForm && !this.props.singleColumn) {
|
||||||
const { onMount } = this.props;
|
const { left, right } = this.composeForm.getBoundingClientRect();
|
||||||
if (onMount) {
|
if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
|
||||||
onMount();
|
this.composeForm.scrollIntoView();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Tells our state the composer has been unmounted.
|
|
||||||
componentWillUnmount () {
|
|
||||||
const { onUnmount } = this.props;
|
|
||||||
if (onUnmount) {
|
|
||||||
onUnmount();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,6 +223,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
preselectDate,
|
preselectDate,
|
||||||
text,
|
text,
|
||||||
preselectOnReply,
|
preselectOnReply,
|
||||||
|
singleColumn,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
let selectionEnd, selectionStart;
|
let selectionEnd, selectionStart;
|
||||||
|
|
||||||
|
@ -246,7 +243,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
textarea.setSelectionRange(selectionStart, selectionEnd);
|
textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
textarea.scrollIntoView();
|
if (!singleColumn) textarea.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refocuses the textarea after submitting.
|
// Refocuses the textarea after submitting.
|
||||||
|
@ -307,7 +304,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
<ReplyIndicatorContainer />
|
<ReplyIndicatorContainer />
|
||||||
|
|
||||||
<div className={`composer--spoiler ${spoiler ? 'composer--spoiler--visible' : ''}`}>
|
<div className={`composer--spoiler ${spoiler ? 'composer--spoiler--visible' : ''}`} ref={this.setRef}>
|
||||||
<AutosuggestInput
|
<AutosuggestInput
|
||||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||||
value={spoilerText}
|
value={spoilerText}
|
||||||
|
@ -323,34 +320,32 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
searchTokens={[':']}
|
searchTokens={[':']}
|
||||||
id='glitch.composer.spoiler.input'
|
id='glitch.composer.spoiler.input'
|
||||||
className='spoiler-input__input'
|
className='spoiler-input__input'
|
||||||
|
autoFocus={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='composer--textarea'>
|
<AutosuggestTextarea
|
||||||
<TextareaIcons advancedOptions={advancedOptions} />
|
ref={this.setAutosuggestTextarea}
|
||||||
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
<AutosuggestTextarea
|
disabled={isSubmitting}
|
||||||
ref={this.setAutosuggestTextarea}
|
value={this.props.text}
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
onChange={this.handleChange}
|
||||||
disabled={isSubmitting}
|
suggestions={this.props.suggestions}
|
||||||
value={this.props.text}
|
onFocus={this.handleFocus}
|
||||||
onChange={this.handleChange}
|
onKeyDown={this.handleKeyDown}
|
||||||
suggestions={this.props.suggestions}
|
onSuggestionsFetchRequested={onFetchSuggestions}
|
||||||
onKeyDown={this.handleKeyDown}
|
onSuggestionsClearRequested={onClearSuggestions}
|
||||||
onSuggestionsFetchRequested={onFetchSuggestions}
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
onSuggestionsClearRequested={onClearSuggestions}
|
onPaste={onPaste}
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
||||||
onPaste={onPaste}
|
>
|
||||||
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EmojiPicker onPickEmoji={handleEmoji} />
|
<EmojiPicker onPickEmoji={handleEmoji} />
|
||||||
</div>
|
<TextareaIcons advancedOptions={advancedOptions} />
|
||||||
|
<div className='compose-form__modifiers'>
|
||||||
<div className='compose-form__modifiers'>
|
<UploadFormContainer />
|
||||||
<UploadFormContainer />
|
<PollFormContainer />
|
||||||
<PollFormContainer />
|
</div>
|
||||||
</div>
|
</AutosuggestTextarea>
|
||||||
|
|
||||||
<OptionsContainer
|
<OptionsContainer
|
||||||
advancedOptions={advancedOptions}
|
advancedOptions={advancedOptions}
|
||||||
|
|
|
@ -17,19 +17,21 @@ export default class NavigationBar extends ImmutablePureComponent {
|
||||||
<div className='drawer--account'>
|
<div className='drawer--account'>
|
||||||
<Permalink className='avatar' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
<Permalink className='avatar' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
||||||
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
|
<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>
|
</Permalink>
|
||||||
|
|
||||||
<Permalink className='acct' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
<div className='navigation-bar__profile'>
|
||||||
<strong>@{this.props.account.get('acct')}</strong>
|
<Permalink className='acct' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
||||||
</Permalink>
|
<strong>@{this.props.account.get('acct')}</strong>
|
||||||
|
</Permalink>
|
||||||
|
|
||||||
{ profileLink !== undefined && (
|
{ profileLink !== undefined && (
|
||||||
<a
|
<a
|
||||||
className='edit'
|
className='edit'
|
||||||
href={ profileLink }
|
href={ profileLink }
|
||||||
><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -232,7 +232,7 @@ class ComposerOptions extends ImmutablePureComponent {
|
||||||
|
|
||||||
const contentTypeItems = {
|
const contentTypeItems = {
|
||||||
plain: {
|
plain: {
|
||||||
icon: 'align-left',
|
icon: 'file-text',
|
||||||
name: 'text/plain',
|
name: 'text/plain',
|
||||||
text: <FormattedMessage {...messages.plain} />,
|
text: <FormattedMessage {...messages.plain} />,
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default @injectIntl
|
||||||
class ReplyIndicator extends ImmutablePureComponent {
|
class ReplyIndicator extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
onCancel: PropTypes.func,
|
onCancel: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,10 +33,10 @@ class SearchPopout extends React.PureComponent {
|
||||||
const { style } = this.props;
|
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' />;
|
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 (
|
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 }) }}>
|
<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 }) => (
|
{({ opacity, scaleX, scaleY }) => (
|
||||||
<div className='drawer--search--popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||||
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
|
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -60,6 +60,10 @@ class SearchPopout extends React.PureComponent {
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
class Search extends React.PureComponent {
|
class Search extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
submitted: PropTypes.bool,
|
submitted: PropTypes.bool,
|
||||||
|
@ -67,6 +71,7 @@ class Search extends React.PureComponent {
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
onClear: PropTypes.func.isRequired,
|
onClear: PropTypes.func.isRequired,
|
||||||
onShow: PropTypes.func.isRequired,
|
onShow: PropTypes.func.isRequired,
|
||||||
|
openInRoute: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,8 +114,10 @@ class Search extends React.PureComponent {
|
||||||
const { onSubmit } = this.props;
|
const { onSubmit } = this.props;
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
if (onSubmit) {
|
onSubmit();
|
||||||
onSubmit();
|
|
||||||
|
if (this.props.openInRoute) {
|
||||||
|
this.context.router.history.push('/search');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
|
@ -121,14 +128,14 @@ class Search extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { intl, value, submitted } = this.props;
|
const { intl, value, submitted } = this.props;
|
||||||
const { expanded } = this.state;
|
const { expanded } = this.state;
|
||||||
const active = value.length > 0 || submitted;
|
const hasValue = value.length > 0 || submitted;
|
||||||
const computedClass = classNames('drawer--search', { active });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={computedClass}>
|
<div className='search'>
|
||||||
<label>
|
<label>
|
||||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
|
<span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
|
||||||
<input
|
<input
|
||||||
|
className='search__input'
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
value={value || ''}
|
value={value || ''}
|
||||||
|
@ -138,17 +145,19 @@ class Search extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
aria-label={intl.formatMessage(messages.placeholder)}
|
aria-label={intl.formatMessage(messages.placeholder)}
|
||||||
className='icon'
|
className='search__icon'
|
||||||
onClick={this.handleClear}
|
onClick={this.handleClear}
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
>
|
>
|
||||||
<Icon icon='search' />
|
<Icon icon='search' className={hasValue ? '' : 'active'} />
|
||||||
<Icon icon='times-circle' />
|
<Icon icon='times-circle' className={hasValue ? 'active' : ''} />
|
||||||
</div>
|
</div>
|
||||||
<Overlay show={expanded && !active} placement='bottom' target={this}>
|
|
||||||
|
<Overlay show={expanded && !hasValue} placement='bottom' target={this}>
|
||||||
<SearchPopout />
|
<SearchPopout />
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
import { searchEnabled } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
|
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
|
||||||
|
@ -20,6 +21,7 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
suggestions: ImmutablePropTypes.list.isRequired,
|
suggestions: ImmutablePropTypes.list.isRequired,
|
||||||
fetchSuggestions: PropTypes.func.isRequired,
|
fetchSuggestions: PropTypes.func.isRequired,
|
||||||
dismissSuggestion: PropTypes.func.isRequired,
|
dismissSuggestion: PropTypes.func.isRequired,
|
||||||
|
searchTerm: PropTypes.string,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,8 +29,8 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
this.props.fetchSuggestions();
|
this.props.fetchSuggestions();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render () {
|
||||||
const { intl, results, suggestions, dismissSuggestion } = this.props;
|
const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props;
|
||||||
|
|
||||||
if (results.isEmpty() && !suggestions.isEmpty()) {
|
if (results.isEmpty() && !suggestions.isEmpty()) {
|
||||||
return (
|
return (
|
||||||
|
@ -51,6 +53,16 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) {
|
||||||
|
statuses = (
|
||||||
|
<section>
|
||||||
|
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
|
||||||
|
|
||||||
|
<div className='search-results__info'>
|
||||||
|
<FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching toots by their content is not enabled on this Mastodon server.' />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let accounts, statuses, hashtags;
|
let accounts, statuses, hashtags;
|
||||||
|
|
|
@ -9,10 +9,8 @@ import {
|
||||||
clearComposeSuggestions,
|
clearComposeSuggestions,
|
||||||
fetchComposeSuggestions,
|
fetchComposeSuggestions,
|
||||||
insertEmojiCompose,
|
insertEmojiCompose,
|
||||||
mountCompose,
|
|
||||||
selectComposeSuggestion,
|
selectComposeSuggestion,
|
||||||
submitCompose,
|
submitCompose,
|
||||||
unmountCompose,
|
|
||||||
uploadCompose,
|
uploadCompose,
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
import {
|
import {
|
||||||
|
@ -114,14 +112,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(changeComposeVisibility(value));
|
dispatch(changeComposeVisibility(value));
|
||||||
},
|
},
|
||||||
|
|
||||||
onMount() {
|
|
||||||
dispatch(mountCompose());
|
|
||||||
},
|
|
||||||
|
|
||||||
onUnmount() {
|
|
||||||
dispatch(unmountCompose());
|
|
||||||
},
|
|
||||||
|
|
||||||
onMediaDescriptionConfirm(routerHistory) {
|
onMediaDescriptionConfirm(routerHistory) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.missingDescriptionMessage),
|
message: intl.formatMessage(messages.missingDescriptionMessage),
|
||||||
|
|
|
@ -16,7 +16,7 @@ function mapStateToProps (state) {
|
||||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
||||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||||
hasPoll: !!poll,
|
hasPoll: !!poll,
|
||||||
allowMedia: !poll && (media ? media.size < 4 && !media.some(item => item.get('type') === 'video') : true),
|
allowMedia: !poll && (media ? media.size < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : true),
|
||||||
hasMedia: media && !!media.size,
|
hasMedia: media && !!media.size,
|
||||||
allowPoll: !(media && !!media.size),
|
allowPoll: !(media && !!media.size),
|
||||||
showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),
|
showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestion
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
results: state.getIn(['search', 'results']),
|
results: state.getIn(['search', 'results']),
|
||||||
suggestions: state.getIn(['suggestions', 'items']),
|
suggestions: state.getIn(['suggestions', 'items']),
|
||||||
|
searchTerm: state.getIn(['search', 'searchTerm']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -4,6 +4,7 @@ import NavigationContainer from './containers/navigation_container';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { mountCompose, unmountCompose } from 'flavours/glitch/actions/compose';
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import SearchContainer from './containers/search_container';
|
import SearchContainer from './containers/search_container';
|
||||||
|
@ -27,9 +28,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
onClickElefriend () {
|
onClickElefriend () {
|
||||||
dispatch(cycleElefriendCompose());
|
dispatch(cycleElefriendCompose());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onMount () {
|
||||||
|
dispatch(mountCompose());
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnmount () {
|
||||||
|
dispatch(unmountCompose());
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class Compose extends React.PureComponent {
|
class Compose extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -38,9 +47,27 @@ class Compose extends React.PureComponent {
|
||||||
isSearchPage: PropTypes.bool,
|
isSearchPage: PropTypes.bool,
|
||||||
elefriend: PropTypes.number,
|
elefriend: PropTypes.number,
|
||||||
onClickElefriend: PropTypes.func,
|
onClickElefriend: PropTypes.func,
|
||||||
|
onMount: PropTypes.func,
|
||||||
|
onUnmount: PropTypes.func,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { isSearchPage } = this.props;
|
||||||
|
|
||||||
|
if (!isSearchPage) {
|
||||||
|
this.props.onMount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
const { isSearchPage } = this.props;
|
||||||
|
|
||||||
|
if (!isSearchPage) {
|
||||||
|
this.props.onUnmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
elefriend,
|
elefriend,
|
||||||
|
@ -61,12 +88,12 @@ class Compose extends React.PureComponent {
|
||||||
<div className='drawer__pager'>
|
<div className='drawer__pager'>
|
||||||
{!isSearchPage && <div className='drawer__inner'>
|
{!isSearchPage && <div className='drawer__inner'>
|
||||||
<NavigationContainer />
|
<NavigationContainer />
|
||||||
|
|
||||||
<ComposeFormContainer />
|
<ComposeFormContainer />
|
||||||
{multiColumn && (
|
|
||||||
<div className='drawer__inner__mastodon'>
|
<div className='drawer__inner__mastodon'>
|
||||||
{mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
|
{mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||||
|
|
||||||
|
export default class Conversation extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
conversationId: PropTypes.string.isRequired,
|
||||||
|
accounts: ImmutablePropTypes.list.isRequired,
|
||||||
|
lastStatusId: PropTypes.string,
|
||||||
|
unread:PropTypes.bool.isRequired,
|
||||||
|
onMoveUp: PropTypes.func,
|
||||||
|
onMoveDown: PropTypes.func,
|
||||||
|
markRead: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
if (!this.context.router) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lastStatusId, unread, markRead } = this.props;
|
||||||
|
|
||||||
|
if (unread) {
|
||||||
|
markRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.router.history.push(`/statuses/${lastStatusId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHotkeyMoveUp = () => {
|
||||||
|
this.props.onMoveUp(this.props.conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHotkeyMoveDown = () => {
|
||||||
|
this.props.onMoveDown(this.props.conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { accounts, lastStatusId, unread } = this.props;
|
||||||
|
|
||||||
|
if (lastStatusId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusContainer
|
||||||
|
id={lastStatusId}
|
||||||
|
unread={unread}
|
||||||
|
otherAccounts={accounts}
|
||||||
|
onMoveUp={this.handleHotkeyMoveUp}
|
||||||
|
onMoveDown={this.handleHotkeyMoveDown}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ConversationContainer from '../containers/conversation_container';
|
||||||
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
export default class ConversationsList extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
conversations: ImmutablePropTypes.list.isRequired,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
onLoadMore: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id)
|
||||||
|
|
||||||
|
handleMoveUp = id => {
|
||||||
|
const elementIndex = this.getCurrentIndex(id) - 1;
|
||||||
|
this._selectChild(elementIndex, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMoveDown = id => {
|
||||||
|
const elementIndex = this.getCurrentIndex(id) + 1;
|
||||||
|
this._selectChild(elementIndex, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadOlder = debounce(() => {
|
||||||
|
const last = this.props.conversations.last();
|
||||||
|
|
||||||
|
if (last && last.get('last_status')) {
|
||||||
|
this.props.onLoadMore(last.get('last_status'));
|
||||||
|
}
|
||||||
|
}, 300, { leading: true })
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { conversations, onLoadMore, ...other } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} scrollKey='direct' ref={this.setRef}>
|
||||||
|
{conversations.map(item => (
|
||||||
|
<ConversationContainer
|
||||||
|
key={item.get('id')}
|
||||||
|
conversationId={item.get('id')}
|
||||||
|
onMoveUp={this.handleMoveUp}
|
||||||
|
onMoveDown={this.handleMoveDown}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Conversation from '../components/conversation';
|
||||||
|
import { markConversationRead } from '../../../actions/conversations';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { conversationId }) => {
|
||||||
|
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
|
||||||
|
unread: conversation.get('unread'),
|
||||||
|
lastStatusId: conversation.get('last_status', null),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { conversationId }) => ({
|
||||||
|
markRead: () => dispatch(markConversationRead(conversationId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Conversation);
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ConversationsList from '../components/conversations_list';
|
||||||
|
import { expandConversations } from 'flavours/glitch/actions/conversations';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
conversations: state.getIn(['conversations', 'items']),
|
||||||
|
isLoading: state.getIn(['conversations', 'isLoading'], true),
|
||||||
|
hasMore: state.getIn(['conversations', 'hasMore'], false),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onLoadMore: maxId => dispatch(expandConversations({ maxId })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList);
|
|
@ -5,10 +5,13 @@ import StatusListContainer from 'flavours/glitch/features/ui/containers/status_l
|
||||||
import Column from 'flavours/glitch/components/column';
|
import Column from 'flavours/glitch/components/column';
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import { expandDirectTimeline } from 'flavours/glitch/actions/timelines';
|
import { expandDirectTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
|
import { mountConversations, unmountConversations, expandConversations } from 'flavours/glitch/actions/conversations';
|
||||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import { connectDirectStream } from 'flavours/glitch/actions/streaming';
|
import { connectDirectStream } from 'flavours/glitch/actions/streaming';
|
||||||
|
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||||
|
import ConversationsListContainer from './containers/conversations_list_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
||||||
|
@ -16,6 +19,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
|
||||||
|
conversationsMode: state.getIn(['settings', 'direct', 'conversations']),
|
||||||
});
|
});
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
|
@ -28,6 +32,7 @@ export default class DirectTimeline extends React.PureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
conversationsMode: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePin = () => {
|
handlePin = () => {
|
||||||
|
@ -50,13 +55,32 @@ export default class DirectTimeline extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch } = this.props;
|
const { dispatch, conversationsMode } = this.props;
|
||||||
|
|
||||||
|
dispatch(mountConversations());
|
||||||
|
|
||||||
|
if (conversationsMode) {
|
||||||
|
dispatch(expandConversations());
|
||||||
|
} else {
|
||||||
|
dispatch(expandDirectTimeline());
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(expandDirectTimeline());
|
|
||||||
this.disconnect = dispatch(connectDirectStream());
|
this.disconnect = dispatch(connectDirectStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { dispatch, conversationsMode } = this.props;
|
||||||
|
|
||||||
|
if (prevProps.conversationsMode && !conversationsMode) {
|
||||||
|
dispatch(expandDirectTimeline());
|
||||||
|
} else if (!prevProps.conversationsMode && conversationsMode) {
|
||||||
|
dispatch(expandConversations());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
this.props.dispatch(unmountConversations());
|
||||||
|
|
||||||
if (this.disconnect) {
|
if (this.disconnect) {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
this.disconnect = null;
|
this.disconnect = null;
|
||||||
|
@ -67,14 +91,49 @@ export default class DirectTimeline extends React.PureComponent {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = maxId => {
|
handleLoadMoreTimeline = maxId => {
|
||||||
this.props.dispatch(expandDirectTimeline({ maxId }));
|
this.props.dispatch(expandDirectTimeline({ maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLoadMoreConversations = maxId => {
|
||||||
|
this.props.dispatch(expandConversations({ maxId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTimelineClick = () => {
|
||||||
|
this.props.dispatch(changeSetting(['direct', 'conversations'], false));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConversationsClick = () => {
|
||||||
|
this.props.dispatch(changeSetting(['direct', 'conversations'], true));
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId, multiColumn } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, conversationsMode } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
|
||||||
|
let contents;
|
||||||
|
if (conversationsMode) {
|
||||||
|
contents = (
|
||||||
|
<ConversationsListContainer
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`direct_timeline-${columnId}`}
|
||||||
|
timelineId='direct'
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
contents = (
|
||||||
|
<StatusListContainer
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`direct_timeline-${columnId}`}
|
||||||
|
timelineId='direct'
|
||||||
|
onLoadMore={this.handleLoadMoreTimeline}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
|
@ -90,13 +149,28 @@ export default class DirectTimeline extends React.PureComponent {
|
||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<StatusListContainer
|
<div className='notification__filter-bar'>
|
||||||
trackScroll={!pinned}
|
<button
|
||||||
scrollKey={`direct_timeline-${columnId}`}
|
className={conversationsMode ? 'active' : ''}
|
||||||
timelineId='direct'
|
onClick={this.handleConversationsClick}
|
||||||
onLoadMore={this.handleLoadMore}
|
>
|
||||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
<FormattedMessage
|
||||||
/>
|
id='direct.conversations_mode'
|
||||||
|
defaultMessage='Conversations'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={conversationsMode ? '' : 'active'}
|
||||||
|
onClick={this.handleTimelineClick}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='direct.timeline_mode'
|
||||||
|
defaultMessage='Timeline'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{contents}
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Overlay from 'react-overlays/lib/Overlay';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import detectPassiveEvents from 'detect-passive-events';
|
import detectPassiveEvents from 'detect-passive-events';
|
||||||
import { buildCustomEmojis } from 'flavours/glitch/util/emoji';
|
import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
|
@ -110,19 +110,6 @@ let EmojiPicker, Emoji; // load asynchronously
|
||||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`;
|
const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`;
|
||||||
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
|
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
|
||||||
|
|
||||||
const categoriesSort = [
|
|
||||||
'recent',
|
|
||||||
'custom',
|
|
||||||
'people',
|
|
||||||
'nature',
|
|
||||||
'foods',
|
|
||||||
'activity',
|
|
||||||
'places',
|
|
||||||
'objects',
|
|
||||||
'symbols',
|
|
||||||
'flags',
|
|
||||||
];
|
|
||||||
|
|
||||||
class ModifierPickerMenu extends React.PureComponent {
|
class ModifierPickerMenu extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -320,8 +307,23 @@ class EmojiPickerMenu extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = intl.formatMessage(messages.emoji);
|
const title = intl.formatMessage(messages.emoji);
|
||||||
|
|
||||||
const { modifierOpen } = this.state;
|
const { modifierOpen } = this.state;
|
||||||
|
|
||||||
|
const categoriesSort = [
|
||||||
|
'recent',
|
||||||
|
'people',
|
||||||
|
'nature',
|
||||||
|
'foods',
|
||||||
|
'activity',
|
||||||
|
'places',
|
||||||
|
'objects',
|
||||||
|
'symbols',
|
||||||
|
'flags',
|
||||||
|
];
|
||||||
|
|
||||||
|
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default class FollowRequests extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column name='follow-requests' icon='users' heading={intl.formatMessage(messages.heading)}>
|
<Column name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}>
|
||||||
<ColumnBackButtonSlim />
|
<ColumnBackButtonSlim />
|
||||||
|
|
||||||
<ScrollContainer scrollKey='follow_requests' shouldUpdateScroll={this.shouldUpdateScroll}>
|
<ScrollContainer scrollKey='follow_requests' shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||||
|
|
|
@ -8,12 +8,14 @@ import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { me, invitesEnabled, version } from 'flavours/glitch/util/initial_state';
|
import { me } from 'flavours/glitch/util/initial_state';
|
||||||
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
|
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchLists } from 'flavours/glitch/actions/lists';
|
import { fetchLists } from 'flavours/glitch/actions/lists';
|
||||||
import { preferencesLink, profileLink, signOutLink } from 'flavours/glitch/util/backend_links';
|
import { preferencesLink, signOutLink } from 'flavours/glitch/util/backend_links';
|
||||||
|
import NavigationBar from '../compose/components/navigation_bar';
|
||||||
|
import LinkFooter from 'flavours/glitch/features/ui/components/link_footer';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
|
@ -73,9 +75,15 @@ const badgeDisplay = (number, limit) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@connect(makeMapStateToProps, mapDispatchToProps)
|
const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
||||||
@injectIntl
|
|
||||||
export default class GettingStarted extends ImmutablePureComponent {
|
export default @connect(makeMapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
|
class GettingStarted extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
@ -95,7 +103,12 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
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')) {
|
if (myAccount.get('locked')) {
|
||||||
fetchFollowRequests();
|
fetchFollowRequests();
|
||||||
|
@ -135,7 +148,7 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (myAccount.get('locked')) {
|
if (myAccount.get('locked')) {
|
||||||
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
navItems.push(<ColumnLink key='6' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
navItems.push(<ColumnLink key='7' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
|
navItems.push(<ColumnLink key='7' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
|
||||||
|
@ -153,7 +166,8 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||||
<Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
|
<Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
|
||||||
<div className='scrollable optionally-scrollable'>
|
<div className='scrollable optionally-scrollable'>
|
||||||
<div className='getting-started__wrapper'>
|
<div className='getting-started__wrapper'>
|
||||||
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
|
{!multiColumn && <NavigationBar account={myAccount} />}
|
||||||
|
{multiColumn && <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />}
|
||||||
{navItems}
|
{navItems}
|
||||||
<ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} />
|
<ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} />
|
||||||
{listItems}
|
{listItems}
|
||||||
|
@ -163,25 +177,7 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href={signOutLink} method='delete' />
|
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href={signOutLink} method='delete' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='getting-started__footer'>
|
<LinkFooter />
|
||||||
<ul>
|
|
||||||
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></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='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id='getting_started.open_source_notice'
|
|
||||||
defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.'
|
|
||||||
values={{
|
|
||||||
github: <span><a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a> (v{version})</span>,
|
|
||||||
Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a> }}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -71,6 +71,10 @@ export default class KeyboardShortcuts extends ImmutablePureComponent {
|
||||||
<td><kbd>x</kbd></td>
|
<td><kbd>x</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>h</kbd></td>
|
||||||
|
<td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
|
||||||
|
</tr>
|
||||||
{collapseEnabled && (
|
{collapseEnabled && (
|
||||||
<tr>
|
<tr>
|
||||||
<td><kbd>shift</kbd>+<kbd>x</kbd></td>
|
<td><kbd>shift</kbd>+<kbd>x</kbd></td>
|
||||||
|
|
|
@ -75,6 +75,23 @@ export default class ListTimeline extends React.PureComponent {
|
||||||
this.disconnect = dispatch(connectListStream(id));
|
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 () {
|
componentWillUnmount () {
|
||||||
if (this.disconnect) {
|
if (this.disconnect) {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
|
|
|
@ -13,6 +13,7 @@ const messages = defineMessages({
|
||||||
general: { id: 'settings.general', defaultMessage: 'General' },
|
general: { id: 'settings.general', defaultMessage: 'General' },
|
||||||
compose: { id: 'settings.compose_box_opts', defaultMessage: 'Compose box' },
|
compose: { id: 'settings.compose_box_opts', defaultMessage: 'Compose box' },
|
||||||
content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' },
|
content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' },
|
||||||
|
filters: { id: 'settings.filters', defaultMessage: 'Filters' },
|
||||||
collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' },
|
collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' },
|
||||||
media: { id: 'settings.media', defaultMessage: 'Media' },
|
media: { id: 'settings.media', defaultMessage: 'Media' },
|
||||||
preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },
|
preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },
|
||||||
|
@ -60,27 +61,34 @@ export default class LocalSettingsNavigation extends React.PureComponent {
|
||||||
active={index === 3}
|
active={index === 3}
|
||||||
index={3}
|
index={3}
|
||||||
onNavigate={onNavigate}
|
onNavigate={onNavigate}
|
||||||
icon='angle-double-up'
|
icon='filter'
|
||||||
title={intl.formatMessage(messages.collapsed)}
|
title={intl.formatMessage(messages.filters)}
|
||||||
/>
|
/>
|
||||||
<LocalSettingsNavigationItem
|
<LocalSettingsNavigationItem
|
||||||
active={index === 4}
|
active={index === 4}
|
||||||
index={4}
|
index={4}
|
||||||
onNavigate={onNavigate}
|
onNavigate={onNavigate}
|
||||||
|
icon='angle-double-up'
|
||||||
|
title={intl.formatMessage(messages.collapsed)}
|
||||||
|
/>
|
||||||
|
<LocalSettingsNavigationItem
|
||||||
|
active={index === 5}
|
||||||
|
index={5}
|
||||||
|
onNavigate={onNavigate}
|
||||||
icon='image'
|
icon='image'
|
||||||
title={intl.formatMessage(messages.media)}
|
title={intl.formatMessage(messages.media)}
|
||||||
/>
|
/>
|
||||||
<LocalSettingsNavigationItem
|
<LocalSettingsNavigationItem
|
||||||
active={index === 5}
|
active={index === 6}
|
||||||
href={ preferencesLink }
|
href={ preferencesLink }
|
||||||
index={5}
|
index={6}
|
||||||
icon='sliders'
|
icon='cog'
|
||||||
title={intl.formatMessage(messages.preferences)}
|
title={intl.formatMessage(messages.preferences)}
|
||||||
/>
|
/>
|
||||||
<LocalSettingsNavigationItem
|
<LocalSettingsNavigationItem
|
||||||
active={index === 6}
|
active={index === 7}
|
||||||
className='close'
|
className='close'
|
||||||
index={6}
|
index={7}
|
||||||
onNavigate={onClose}
|
onNavigate={onClose}
|
||||||
icon='times'
|
icon='times'
|
||||||
title={intl.formatMessage(messages.close)}
|
title={intl.formatMessage(messages.close)}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue