mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-15 00:38:27 +00:00
Compare commits
232 Commits
ThibG-patc
...
glitch-soc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83247d7617 | ||
|
|
3f4e8fa2c2 | ||
|
|
34293ec05b | ||
|
|
98c2d2aa46 | ||
|
|
662252c8f7 | ||
|
|
4d964398de | ||
|
|
3922b518f7 | ||
|
|
4f5b221be2 | ||
|
|
f7c0e326ab | ||
|
|
43698e08ca | ||
|
|
e64e6a03dd | ||
|
|
072158ee97 | ||
|
|
c8ba75b963 | ||
|
|
6ad870a410 | ||
|
|
ca8944728f | ||
|
|
9a90ec3b3b | ||
|
|
ca17bae904 | ||
|
|
2f95adc06f | ||
|
|
6ab7051b48 | ||
|
|
a02f4b7cd4 | ||
|
|
4175f13155 | ||
|
|
383136d9bb | ||
|
|
5c3171e8ea | ||
|
|
aaec64a500 | ||
|
|
3086c645fd | ||
|
|
915c619394 | ||
|
|
32a4494926 | ||
|
|
9ef25877df | ||
|
|
5b20284f6f | ||
|
|
ed10ae2693 | ||
|
|
07508b2045 | ||
|
|
6836587117 | ||
|
|
8d57795608 | ||
|
|
8d56433327 | ||
|
|
1afb8cac2f | ||
|
|
d9ac5b79ae | ||
|
|
864bc0d97a | ||
|
|
99ade565b4 | ||
|
|
f1da937245 | ||
|
|
e76d2c51c2 | ||
|
|
47ef4a6c7a | ||
|
|
81bf43cfdd | ||
|
|
598cdc9542 | ||
|
|
d7eb580053 | ||
|
|
967456b6a9 | ||
|
|
ddd875ad99 | ||
|
|
66ac1bd063 | ||
|
|
b5c772c3d4 | ||
|
|
8fe7116cdf | ||
|
|
b927bb3f07 | ||
|
|
6eb5241099 | ||
|
|
d61d164685 | ||
|
|
aa9b37822b | ||
|
|
84f945d64c | ||
|
|
6e7e714bd9 | ||
|
|
1b052c7b2d | ||
|
|
707ddf7808 | ||
|
|
49ebda4d49 | ||
|
|
38d2882447 | ||
|
|
bb9459774d | ||
|
|
f717d7a92d | ||
|
|
8f23726918 | ||
|
|
7696f77245 | ||
|
|
33144e132d | ||
|
|
f7f23b4a19 | ||
|
|
3771a993b7 | ||
|
|
032a669622 | ||
|
|
356e9150df | ||
|
|
e9a11dca19 | ||
|
|
26d3b2efce | ||
|
|
f4b008906d | ||
|
|
ed2f0f6152 | ||
|
|
a06e7bc3fb | ||
|
|
ede0be5dba | ||
|
|
5ea45351a0 | ||
|
|
c8fae508cf | ||
|
|
17747e2cd7 | ||
|
|
48ec6abaca | ||
|
|
83dd4d4204 | ||
|
|
b403c33fb4 | ||
|
|
7555a0017e | ||
|
|
85ec79cedf | ||
|
|
54438042f1 | ||
|
|
119cb4d473 | ||
|
|
9639a7f87a | ||
|
|
10c4c21298 | ||
|
|
f4539845e0 | ||
|
|
31eed31e37 | ||
|
|
efb07f177d | ||
|
|
bab2231470 | ||
|
|
a9ba6a880e | ||
|
|
e67f38020f | ||
|
|
04b4d2b4fa | ||
|
|
7b058c5687 | ||
|
|
f765cd97b2 | ||
|
|
103a9f4466 | ||
|
|
f57a0f89a8 | ||
|
|
118701b548 | ||
|
|
5717f75340 | ||
|
|
d3aaacb6d4 | ||
|
|
65efe892cf | ||
|
|
01e362316c | ||
|
|
0828126784 | ||
|
|
a29ab6f1bd | ||
|
|
b6f76d1306 | ||
|
|
e433386545 | ||
|
|
54192a9b6f | ||
|
|
c0e5f32d13 | ||
|
|
0fd7a8b63d | ||
|
|
7b68e1725c | ||
|
|
60adda7e59 | ||
|
|
80849812d2 | ||
|
|
d1edbfaed3 | ||
|
|
983cbd558d | ||
|
|
0f41be5581 | ||
|
|
0ccc06b87f | ||
|
|
a773d02946 | ||
|
|
44b1a39682 | ||
|
|
9400ec43cc | ||
|
|
127ead34c4 | ||
|
|
b191861e15 | ||
|
|
47307e6c13 | ||
|
|
b222d1ae26 | ||
|
|
7fd8797d20 | ||
|
|
1b130f964f | ||
|
|
362f3973be | ||
|
|
3485acefed | ||
|
|
867d1233c7 | ||
|
|
8f924eb961 | ||
|
|
a5398c3df8 | ||
|
|
1329308bc7 | ||
|
|
02d6187894 | ||
|
|
763735f92e | ||
|
|
9bb4f796db | ||
|
|
c095eed121 | ||
|
|
ff88387a4a | ||
|
|
d99a661f08 | ||
|
|
0bd9f23e6d | ||
|
|
610b4b44c4 | ||
|
|
c9eeb2e832 | ||
|
|
5c7f1e8e2f | ||
|
|
e6024d610d | ||
|
|
b4d67fe57a | ||
|
|
dd45c63921 | ||
|
|
917f0ea619 | ||
|
|
fe5c4f976c | ||
|
|
275f09ccab | ||
|
|
a4a502e85c | ||
|
|
1b4dcc3f78 | ||
|
|
c98573fdf9 | ||
|
|
faafc3ae25 | ||
|
|
809d1faa49 | ||
|
|
1390da501d | ||
|
|
481cc19d4d | ||
|
|
da33c94c14 | ||
|
|
d13bc8eb6a | ||
|
|
32bdff09c1 | ||
|
|
7652190509 | ||
|
|
cde30407c6 | ||
|
|
963d7e0377 | ||
|
|
82899b3d2e | ||
|
|
7065ba5930 | ||
|
|
4913c345b2 | ||
|
|
30e2f724ce | ||
|
|
69c2bbcc87 | ||
|
|
02323aa1d8 | ||
|
|
40be49fe28 | ||
|
|
92b572e2a3 | ||
|
|
c64eef1206 | ||
|
|
ef438bd7e8 | ||
|
|
7778de467c | ||
|
|
5bcd98172c | ||
|
|
62852252dd | ||
|
|
fc6d27daf3 | ||
|
|
59d214e54b | ||
|
|
1b0ff4cd69 | ||
|
|
b45f555a0c | ||
|
|
b551d8aa53 | ||
|
|
ccfb48d3eb | ||
|
|
d61a6271c6 | ||
|
|
8514ef723c | ||
|
|
210fa3a94e | ||
|
|
420551872d | ||
|
|
0ef55b341d | ||
|
|
fbe879ceaf | ||
|
|
ce556333ce | ||
|
|
4330629101 | ||
|
|
e428e320b6 | ||
|
|
e16c8fbc7a | ||
|
|
654fd071b7 | ||
|
|
9a281bac8d | ||
|
|
0949c43ab3 | ||
|
|
e5bdfd1640 | ||
|
|
8746f4d17b | ||
|
|
9add88a920 | ||
|
|
6ecf1825ef | ||
|
|
241a8e7b5f | ||
|
|
4431ce52a6 | ||
|
|
537e928186 | ||
|
|
e9ddd5a159 | ||
|
|
20dda5cca0 | ||
|
|
f4bc77f290 | ||
|
|
25f93f4097 | ||
|
|
11c28abcfe | ||
|
|
aec3fa35fd | ||
|
|
417989ae34 | ||
|
|
b32a62fe95 | ||
|
|
cc8f6b3cda | ||
|
|
01aae33a5f | ||
|
|
34b8346e7f | ||
|
|
560ec24e58 | ||
|
|
8360019896 | ||
|
|
cdb420862e | ||
|
|
62e6a29f0b | ||
|
|
1db4117030 | ||
|
|
a60364ca7d | ||
|
|
c672676c03 | ||
|
|
a03fb7b703 | ||
|
|
fe3bf3b0fc | ||
|
|
a7f6e72b30 | ||
|
|
5bfd802c57 | ||
|
|
2657765d2a | ||
|
|
70423ce81f | ||
|
|
c402c291f4 | ||
|
|
8f3c32e29c | ||
|
|
cac9110533 | ||
|
|
7fa23ec697 | ||
|
|
d34a3a2cc7 | ||
|
|
6c464cd424 | ||
|
|
ed19f33440 | ||
|
|
6a9a759f40 | ||
|
|
f2b743e715 |
@@ -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:
|
||||||
|
|||||||
@@ -169,15 +169,12 @@ STREAMING_CLUSTER_NUM=1
|
|||||||
# Maximum allowed display name characters
|
# Maximum allowed display name characters
|
||||||
# MAX_DISPLAY_NAME_CHARS=30
|
# MAX_DISPLAY_NAME_CHARS=30
|
||||||
|
|
||||||
# Maximum image and video upload sizes
|
# Maximum image and video/audio upload sizes
|
||||||
# Units are in bytes
|
# Units are in bytes
|
||||||
# 1048576 bytes equals 1 megabyte
|
# 1048576 bytes equals 1 megabyte
|
||||||
# MAX_IMAGE_SIZE=8388608
|
# MAX_IMAGE_SIZE=8388608
|
||||||
# MAX_VIDEO_SIZE=41943040
|
# MAX_VIDEO_SIZE=41943040
|
||||||
|
|
||||||
# Maximum length of audio uploads in seconds
|
|
||||||
# MAX_AUDIO_LENGTH=60
|
|
||||||
|
|
||||||
# LDAP authentication (optional)
|
# LDAP authentication (optional)
|
||||||
# LDAP_ENABLED=true
|
# LDAP_ENABLED=true
|
||||||
# LDAP_HOST=localhost
|
# LDAP_HOST=localhost
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
281
.sass-lint.yml
281
.sass-lint.yml
@@ -4,261 +4,34 @@
|
|||||||
files:
|
files:
|
||||||
include: app/javascript/styles/**/*.scss
|
include: app/javascript/styles/**/*.scss
|
||||||
ignore:
|
ignore:
|
||||||
- app/javascript/styles/reset.scss
|
- app/javascript/styles/mastodon/reset.scss
|
||||||
|
|
||||||
linters:
|
rules:
|
||||||
# Reports when you use improper spacing around ! (the "bang") in !default,
|
# Disallows
|
||||||
# !global, !important, and !optional flags.
|
no-color-literals: 0
|
||||||
BangFormat:
|
no-css-comments: 0
|
||||||
enabled: false
|
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
|
||||||
|
|
||||||
# Whether or not to prefer `border: 0` over `border: none`.
|
# Nesting
|
||||||
BorderZero:
|
force-element-nesting: 0
|
||||||
enabled: false
|
force-attribute-nesting: 0
|
||||||
|
force-pseudo-nesting: 0
|
||||||
|
|
||||||
# Reports when you define a rule set using a selector with chained classes
|
# Name Formats
|
||||||
# (a.k.a. adjoining classes).
|
class-name-format: 0
|
||||||
ChainedClasses:
|
leading-zero: 0
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# Prefer hexadecimal color codes over color keywords.
|
# Style Guide
|
||||||
# (e.g. `color: green` is a color keyword)
|
attribute-quotes: 0
|
||||||
ColorKeyword:
|
hex-length: 0
|
||||||
enabled: false
|
indentation: 0
|
||||||
|
nesting-depth: 0
|
||||||
# Prefer color literals (keywords or hexadecimal codes) to be used only in
|
property-sort-order: 0
|
||||||
# variable declarations. They should be referred to via variables everywhere
|
quotes: 0
|
||||||
# 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
|
|
||||||
|
|||||||
93
CHANGELOG.md
93
CHANGELOG.md
@@ -3,6 +3,99 @@ 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
|
## [2.8.4] - 2019-05-24
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
|
|||||||
9
Gemfile
9
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.41', require: false
|
gem 'aws-sdk-s3', '~> 1.43', 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'
|
||||||
@@ -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'
|
||||||
@@ -111,14 +111,14 @@ group :production, :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.22'
|
gem 'capybara', '~> 3.24'
|
||||||
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.16', require: false
|
||||||
gem 'webmock', '~> 3.5'
|
gem 'webmock', '~> 3.6'
|
||||||
gem 'parallel_tests', '~> 2.29'
|
gem 'parallel_tests', '~> 2.29'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -132,6 +132,7 @@ group :development do
|
|||||||
gem 'letter_opener_web', '~> 1.3'
|
gem 'letter_opener_web', '~> 1.3'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 0.71', require: false
|
gem 'rubocop', '~> 0.71', require: false
|
||||||
|
gem 'rubocop-rails', '~> 2.0', 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
|
||||||
|
|
||||||
|
|||||||
52
Gemfile.lock
52
Gemfile.lock
@@ -76,17 +76,17 @@ 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.169.0)
|
aws-partitions (1.177.0)
|
||||||
aws-sdk-core (3.54.0)
|
aws-sdk-core (3.56.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.21.0)
|
aws-sdk-kms (1.22.0)
|
||||||
aws-sdk-core (~> 3, >= 3.53.0)
|
aws-sdk-core (~> 3, >= 3.56.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.41.0)
|
aws-sdk-s3 (1.43.0)
|
||||||
aws-sdk-core (~> 3, >= 3.53.0)
|
aws-sdk-core (~> 3, >= 3.56.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.1.0)
|
aws-sigv4 (1.1.0)
|
||||||
@@ -129,7 +129,7 @@ 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.22.0)
|
capybara (3.24.0)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
@@ -159,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)
|
||||||
@@ -188,9 +188,9 @@ GEM
|
|||||||
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)
|
||||||
@@ -253,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)
|
||||||
@@ -271,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.3.0)
|
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)
|
||||||
@@ -322,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.1)
|
lograge (0.11.2)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
@@ -384,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.1)
|
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)
|
||||||
@@ -395,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)
|
||||||
@@ -403,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)
|
||||||
@@ -534,12 +534,15 @@ GEM
|
|||||||
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)
|
||||||
|
rubocop-rails (2.0.1)
|
||||||
|
rack (>= 1.1)
|
||||||
|
rubocop (>= 0.70.0)
|
||||||
ruby-progressbar (1.10.1)
|
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)
|
||||||
@@ -621,10 +624,10 @@ 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.7)
|
webpacker (4.0.7)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
@@ -647,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.41)
|
aws-sdk-s3 (~> 1.43)
|
||||||
better_errors (~> 2.5)
|
better_errors (~> 2.5)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
@@ -660,7 +663,7 @@ 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.22)
|
capybara (~> 3.24)
|
||||||
charlock_holmes (~> 0.7.6)
|
charlock_holmes (~> 0.7.6)
|
||||||
chewy (~> 5.0)
|
chewy (~> 5.0)
|
||||||
cld3 (~> 3.2.4)
|
cld3 (~> 3.2.4)
|
||||||
@@ -711,7 +714,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)
|
||||||
@@ -740,6 +743,7 @@ DEPENDENCIES
|
|||||||
rspec-rails (~> 3.8)
|
rspec-rails (~> 3.8)
|
||||||
rspec-sidekiq (~> 3.0)
|
rspec-sidekiq (~> 3.0)
|
||||||
rubocop (~> 0.71)
|
rubocop (~> 0.71)
|
||||||
|
rubocop-rails (~> 2.0)
|
||||||
sanitize (~> 5.0)
|
sanitize (~> 5.0)
|
||||||
sidekiq (~> 5.2)
|
sidekiq (~> 5.2)
|
||||||
sidekiq-bulk (~> 0.2.0)
|
sidekiq-bulk (~> 0.2.0)
|
||||||
@@ -758,7 +762,7 @@ DEPENDENCIES
|
|||||||
tty-prompt (~> 0.19)
|
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
|
||||||
|
|||||||
32
app/controllers/api/v1/admin/account_actions_controller.rb
Normal file
32
app/controllers/api/v1/admin/account_actions_controller.rb
Normal file
@@ -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
|
||||||
128
app/controllers/api/v1/admin/accounts_controller.rb
Normal file
128
app/controllers/api/v1/admin/accounts_controller.rb
Normal file
@@ -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
|
||||||
108
app/controllers/api/v1/admin/reports_controller.rb
Normal file
108
app/controllers/api/v1/admin/reports_controller.rb
Normal file
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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?
|
||||||
@@ -37,6 +39,6 @@ class MediaProxyController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reject_media?
|
def reject_media?
|
||||||
DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media?
|
DomainBlock.reject_media?(@media_attachment.account.domain)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Settings::Preferences::AppearanceController < Settings::PreferencesController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
settings_preferences_appearance_path
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Settings::Preferences::NotificationsController < Settings::PreferencesController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
settings_preferences_notifications_path
|
||||||
|
end
|
||||||
|
end
|
||||||
9
app/controllers/settings/preferences/other_controller.rb
Normal file
9
app/controllers/settings/preferences/other_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Settings::Preferences::OtherController < Settings::PreferencesController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
settings_preferences_other_path
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
|
||||||
@@ -48,8 +52,9 @@ class Settings::PreferencesController < Settings::BaseController
|
|||||||
:setting_show_application,
|
:setting_show_application,
|
||||||
:setting_advanced_layout,
|
:setting_advanced_layout,
|
||||||
:setting_default_content_type,
|
:setting_default_content_type,
|
||||||
|
:setting_use_blurhash,
|
||||||
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)
|
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ module StreamEntriesHelper
|
|||||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||||
end
|
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');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
84
app/javascript/flavours/glitch/actions/conversations.js
Normal file
84
app/javascript/flavours/glitch/actions/conversations.js
Normal file
@@ -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;
|
||||||
|
|||||||
@@ -62,9 +62,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
|
||||||
if (notification.type === 'mention') {
|
if (notification.type === 'mention') {
|
||||||
|
const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible')));
|
||||||
const regex = regexFromFilters(filters);
|
const regex = regexFromFilters(filters);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,7 +200,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||||||
style.direction = 'rtl';
|
style.direction = 'rtl';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return [
|
||||||
|
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||||
<div className='autosuggest-textarea'>
|
<div className='autosuggest-textarea'>
|
||||||
<label>
|
<label>
|
||||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||||
@@ -219,12 +223,16 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||||||
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>,
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
104
app/javascript/flavours/glitch/components/avatar_composite.js
Normal file
104
app/javascript/flavours/glitch/components/avatar_composite.js
Normal file
@@ -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 => (
|
||||||
<span className={computedClass}>
|
<a
|
||||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
|
href={a.get('url')}
|
||||||
{inline ? ' ' : null}
|
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>
|
<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}>
|
||||||
|
{displayName}
|
||||||
|
{inline ? ' ' : null}
|
||||||
|
{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,
|
||||||
};
|
};
|
||||||
|
|||||||
20
app/javascript/flavours/glitch/components/icon_with_badge.js
Normal file
20
app/javascript/flavours/glitch/components/icon_with_badge.js
Normal file
@@ -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,19 +24,20 @@ 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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -66,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,
|
||||||
@@ -83,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,
|
||||||
@@ -93,6 +95,7 @@ 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 = {
|
||||||
@@ -111,8 +114,6 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
'account',
|
'account',
|
||||||
'settings',
|
'settings',
|
||||||
'prepend',
|
'prepend',
|
||||||
'boostModal',
|
|
||||||
'favouriteModal',
|
|
||||||
'muted',
|
'muted',
|
||||||
'collapse',
|
'collapse',
|
||||||
'notification',
|
'notification',
|
||||||
@@ -321,17 +322,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);
|
||||||
@@ -441,6 +446,7 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
intl,
|
intl,
|
||||||
status,
|
status,
|
||||||
account,
|
account,
|
||||||
|
otherAccounts,
|
||||||
settings,
|
settings,
|
||||||
collapsed,
|
collapsed,
|
||||||
muted,
|
muted,
|
||||||
@@ -450,6 +456,7 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
onOpenMedia,
|
onOpenMedia,
|
||||||
notification,
|
notification,
|
||||||
hidden,
|
hidden,
|
||||||
|
unread,
|
||||||
featured,
|
featured,
|
||||||
...other
|
...other
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -514,16 +521,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'])}
|
||||||
@@ -537,7 +544,7 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
/>)}
|
/>)}
|
||||||
</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}>
|
||||||
@@ -617,6 +624,7 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
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');
|
||||||
|
|
||||||
@@ -647,6 +655,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>
|
||||||
@@ -656,6 +665,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
|
||||||
@@ -673,6 +683,7 @@ 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}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{notification ? (
|
{notification ? (
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||||||
onBookmark: PropTypes.func,
|
onBookmark: 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -282,14 +283,15 @@ 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 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 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 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'>
|
<div className='status__action-bar-dropdown'>
|
||||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
|
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
|
||||||
</div>
|
</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,25 +34,30 @@ 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');
|
||||||
|
|
||||||
|
let statusAvatar;
|
||||||
|
if (otherAccounts && otherAccounts.size > 0) {
|
||||||
|
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} onAccountClick={this.handleClick} />;
|
||||||
|
} else if (friend === undefined || friend === null) {
|
||||||
|
statusAvatar = <Avatar account={account} size={48} />;
|
||||||
|
} else {
|
||||||
|
statusAvatar = <AvatarOverlay account={account} friend={friend} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!otherAccounts) {
|
||||||
return (
|
return (
|
||||||
<div className='status__info__account' >
|
<div className='status__info__account'>
|
||||||
<a
|
<a
|
||||||
href={account.get('url')}
|
href={account.get('url')}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='status__avatar'
|
className='status__avatar'
|
||||||
onClick={this.handleAccountClick}
|
onClick={this.handleAccountClick}
|
||||||
>
|
>
|
||||||
{
|
{statusAvatar}
|
||||||
friend ? (
|
|
||||||
<AvatarOverlay account={account} friend={friend} />
|
|
||||||
) : (
|
|
||||||
<Avatar account={account} size={48} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={account.get('url')}
|
href={account.get('url')}
|
||||||
@@ -53,10 +65,24 @@ export default class StatusHeader extends React.PureComponent {
|
|||||||
className='status__display-name'
|
className='status__display-name'
|
||||||
onClick={this.handleAccountClick}
|
onClick={this.handleAccountClick}
|
||||||
>
|
>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} others={otherAccounts} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,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,
|
||||||
};
|
};
|
||||||
@@ -42,6 +43,7 @@ export default class StatusIcons extends React.PureComponent {
|
|||||||
mediaIcon,
|
mediaIcon,
|
||||||
collapsible,
|
collapsible,
|
||||||
collapsed,
|
collapsed,
|
||||||
|
directMessage,
|
||||||
intl,
|
intl,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -59,9 +61,7 @@ export default class StatusIcons extends React.PureComponent {
|
|||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
/>
|
/>
|
||||||
) : 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'
|
||||||
|
|||||||
@@ -96,11 +96,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onReblog (status, e) {
|
onReblog (status, e) {
|
||||||
if (e.shiftKey || !boostModal) {
|
dispatch((_, getState) => {
|
||||||
|
let state = getState();
|
||||||
|
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.handleModalReblog, missingMediaDescription: true }));
|
||||||
|
} else if (e.shiftKey || !boostModal) {
|
||||||
this.onModalReblog(status);
|
this.onModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onBookmark (status) {
|
onBookmark (status) {
|
||||||
|
|||||||
@@ -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,9 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tells our state the composer has been mounted.
|
handleFocus = () => {
|
||||||
componentDidMount () {
|
if (this.composeForm && !this.props.singleColumn) {
|
||||||
const { onMount } = this.props;
|
this.composeForm.scrollIntoView();
|
||||||
if (onMount) {
|
|
||||||
onMount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tells our state the composer has been unmounted.
|
|
||||||
componentWillUnmount () {
|
|
||||||
const { onUnmount } = this.props;
|
|
||||||
if (onUnmount) {
|
|
||||||
onUnmount();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +220,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
preselectDate,
|
preselectDate,
|
||||||
text,
|
text,
|
||||||
preselectOnReply,
|
preselectOnReply,
|
||||||
|
singleColumn,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
let selectionEnd, selectionStart;
|
let selectionEnd, selectionStart;
|
||||||
|
|
||||||
@@ -246,7 +240,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 +301,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,12 +317,10 @@ 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'>
|
|
||||||
<TextareaIcons advancedOptions={advancedOptions} />
|
|
||||||
|
|
||||||
<AutosuggestTextarea
|
<AutosuggestTextarea
|
||||||
ref={this.setAutosuggestTextarea}
|
ref={this.setAutosuggestTextarea}
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
@@ -336,21 +328,21 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
value={this.props.text}
|
value={this.props.text}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
suggestions={this.props.suggestions}
|
suggestions={this.props.suggestions}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
onSuggestionsFetchRequested={onFetchSuggestions}
|
onSuggestionsFetchRequested={onFetchSuggestions}
|
||||||
onSuggestionsClearRequested={onClearSuggestions}
|
onSuggestionsClearRequested={onClearSuggestions}
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
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,9 +17,10 @@ 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>
|
||||||
|
|
||||||
|
<div className='navigation-bar__profile'>
|
||||||
<Permalink className='acct' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
<Permalink className='acct' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
||||||
<strong>@{this.props.account.get('acct')}</strong>
|
<strong>@{this.props.account.get('acct')}</strong>
|
||||||
</Permalink>
|
</Permalink>
|
||||||
@@ -31,6 +32,7 @@ export default class NavigationBar extends ImmutablePureComponent {
|
|||||||
><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} />,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default class LocalSettingsNavigation extends React.PureComponent {
|
|||||||
active={index === 5}
|
active={index === 5}
|
||||||
href={ preferencesLink }
|
href={ preferencesLink }
|
||||||
index={5}
|
index={5}
|
||||||
icon='sliders'
|
icon='cog'
|
||||||
title={intl.formatMessage(messages.preferences)}
|
title={intl.formatMessage(messages.preferences)}
|
||||||
/>
|
/>
|
||||||
<LocalSettingsNavigationItem
|
<LocalSettingsNavigationItem
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import LocalSettingsPageItem from './item';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
layout_auto: { id: 'layout.auto', defaultMessage: 'Auto' },
|
layout_auto: { id: 'layout.auto', defaultMessage: 'Auto' },
|
||||||
|
layout_auto_hint: { id: 'layout.hint.auto', defaultMessage: 'Automatically chose layout based on “Enable advanced web interface” setting and screen size.' },
|
||||||
layout_desktop: { id: 'layout.desktop', defaultMessage: 'Desktop' },
|
layout_desktop: { id: 'layout.desktop', defaultMessage: 'Desktop' },
|
||||||
|
layout_desktop_hint: { id: 'layout.hint.desktop', defaultMessage: 'Use multiple-column layout regardless of the “Enable advanced web interface” setting or screen size.' },
|
||||||
layout_mobile: { id: 'layout.single', defaultMessage: 'Mobile' },
|
layout_mobile: { id: 'layout.single', defaultMessage: 'Mobile' },
|
||||||
|
layout_mobile_hint: { id: 'layout.hint.single', defaultMessage: 'Use single-column layout regardless of the “Enable advanced web interface” setting or screen size.' },
|
||||||
side_arm_none: { id: 'settings.side_arm.none', defaultMessage: 'None' },
|
side_arm_none: { id: 'settings.side_arm.none', defaultMessage: 'None' },
|
||||||
side_arm_keep: { id: 'settings.side_arm_reply_mode.keep', defaultMessage: 'Keep secondary toot button to set privacy' },
|
side_arm_keep: { id: 'settings.side_arm_reply_mode.keep', defaultMessage: 'Keep secondary toot button to set privacy' },
|
||||||
side_arm_copy: { id: 'settings.side_arm_reply_mode.copy', defaultMessage: 'Copy privacy setting of the toot being replied to' },
|
side_arm_copy: { id: 'settings.side_arm_reply_mode.copy', defaultMessage: 'Copy privacy setting of the toot being replied to' },
|
||||||
@@ -51,6 +54,14 @@ export default class LocalSettingsPage extends React.PureComponent {
|
|||||||
<FormattedMessage id='settings.hicolor_privacy_icons' defaultMessage='High color privacy icons' />
|
<FormattedMessage id='settings.hicolor_privacy_icons' defaultMessage='High color privacy icons' />
|
||||||
<span className='hint'><FormattedMessage id='settings.hicolor_privacy_icons.hint' defaultMessage="Display privacy icons in bright and easily distinguishable colors" /></span>
|
<span className='hint'><FormattedMessage id='settings.hicolor_privacy_icons.hint' defaultMessage="Display privacy icons in bright and easily distinguishable colors" /></span>
|
||||||
</LocalSettingsPageItem>
|
</LocalSettingsPageItem>
|
||||||
|
<LocalSettingsPageItem
|
||||||
|
settings={settings}
|
||||||
|
item={['confirm_boost_missing_media_description']}
|
||||||
|
id='mastodon-settings--confirm_boost_missing_media_description'
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='settings.confirm_boost_missing_media_description' defaultMessage='Show confirmation dialog before boosting toots lacking media descriptions' />
|
||||||
|
</LocalSettingsPageItem>
|
||||||
<section>
|
<section>
|
||||||
<h2><FormattedMessage id='settings.notifications_opts' defaultMessage='Notifications options' /></h2>
|
<h2><FormattedMessage id='settings.notifications_opts' defaultMessage='Notifications options' /></h2>
|
||||||
<LocalSettingsPageItem
|
<LocalSettingsPageItem
|
||||||
@@ -79,9 +90,9 @@ export default class LocalSettingsPage extends React.PureComponent {
|
|||||||
item={['layout']}
|
item={['layout']}
|
||||||
id='mastodon-settings--layout'
|
id='mastodon-settings--layout'
|
||||||
options={[
|
options={[
|
||||||
{ value: 'auto', message: intl.formatMessage(messages.layout_auto) },
|
{ value: 'auto', message: intl.formatMessage(messages.layout_auto), hint: intl.formatMessage(messages.layout_auto_hint) },
|
||||||
{ value: 'multiple', message: intl.formatMessage(messages.layout_desktop) },
|
{ value: 'multiple', message: intl.formatMessage(messages.layout_desktop), hint: intl.formatMessage(messages.layout_desktop_hint) },
|
||||||
{ value: 'single', message: intl.formatMessage(messages.layout_mobile) },
|
{ value: 'single', message: intl.formatMessage(messages.layout_mobile), hint: intl.formatMessage(messages.layout_mobile_hint) },
|
||||||
]}
|
]}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
|
|||||||
17
app/javascript/flavours/glitch/features/search/index.js
Normal file
17
app/javascript/flavours/glitch/features/search/index.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import SearchContainer from 'flavours/glitch/features/compose/containers/search_container';
|
||||||
|
import SearchResultsContainer from 'flavours/glitch/features/compose/containers/search_results_container';
|
||||||
|
|
||||||
|
const Search = () => (
|
||||||
|
<div className='column search-page'>
|
||||||
|
<SearchContainer />
|
||||||
|
|
||||||
|
<div className='drawer__pager'>
|
||||||
|
<div className='drawer__inner darker'>
|
||||||
|
<SearchResultsContainer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Search;
|
||||||
@@ -131,14 +131,14 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||||||
} else if (status.get('media_attachments').size > 0) {
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||||
media = <AttachmentList media={status.get('media_attachments')} />;
|
media = <AttachmentList media={status.get('media_attachments')} />;
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
} else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
|
||||||
const video = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
media = (
|
media = (
|
||||||
<Video
|
<Video
|
||||||
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'])}
|
||||||
@@ -150,7 +150,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
mediaIcon = 'video-camera';
|
mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music';
|
||||||
} else {
|
} else {
|
||||||
media = (
|
media = (
|
||||||
<MediaGallery
|
<MediaGallery
|
||||||
|
|||||||
@@ -231,18 +231,24 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleModalReblog = (status) => {
|
handleModalReblog = (status) => {
|
||||||
this.props.dispatch(reblog(status));
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
if (status.get('reblogged')) {
|
||||||
|
dispatch(unreblog(status));
|
||||||
|
} else {
|
||||||
|
dispatch(reblog(status));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReblogClick = (status, e) => {
|
handleReblogClick = (status, e) => {
|
||||||
if (status.get('reblogged')) {
|
const { settings, dispatch } = this.props;
|
||||||
this.props.dispatch(unreblog(status));
|
|
||||||
} else {
|
if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
|
||||||
if ((e && e.shiftKey) || !boostModal) {
|
dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
|
||||||
|
} else if ((e && e.shiftKey) || !boostModal) {
|
||||||
this.handleModalReblog(status);
|
this.handleModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
|
dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import StatusContent from 'flavours/glitch/components/status_content';
|
|||||||
import Avatar from 'flavours/glitch/components/avatar';
|
import Avatar from 'flavours/glitch/components/avatar';
|
||||||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||||
import DisplayName from 'flavours/glitch/components/display_name';
|
import DisplayName from 'flavours/glitch/components/display_name';
|
||||||
|
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -25,6 +26,7 @@ export default class BoostModal extends ImmutablePureComponent {
|
|||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
onReblog: PropTypes.func.isRequired,
|
onReblog: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
missingMediaDescription: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ export default class BoostModal extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl } = this.props;
|
const { status, missingMediaDescription, intl } = this.props;
|
||||||
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -74,11 +76,24 @@ export default class BoostModal extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusContent status={status} />
|
<StatusContent status={status} />
|
||||||
|
|
||||||
|
{status.get('media_attachments').size > 0 && (
|
||||||
|
<AttachmentList
|
||||||
|
compact
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='boost-modal__action-bar'>
|
<div className='boost-modal__action-bar'>
|
||||||
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div>
|
<div>
|
||||||
|
{ missingMediaDescription ?
|
||||||
|
<FormattedMessage id='boost_modal.missing_description' defaultMessage='This toot contains some media without description' />
|
||||||
|
:
|
||||||
|
<FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
|
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import ReactSwipeableViews from 'react-swipeable-views';
|
import ReactSwipeableViews from 'react-swipeable-views';
|
||||||
import { links, getIndex, getLink } from './tabs_bar';
|
import TabsBar, { links, getIndex, getLink } from './tabs_bar';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import BundleContainer from '../containers/bundle_container';
|
import BundleContainer from '../containers/bundle_container';
|
||||||
@@ -13,6 +13,8 @@ import ColumnLoading from './column_loading';
|
|||||||
import DrawerLoading from './drawer_loading';
|
import DrawerLoading from './drawer_loading';
|
||||||
import BundleColumnError from './bundle_column_error';
|
import BundleColumnError from './bundle_column_error';
|
||||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, BookmarkedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
|
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, BookmarkedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
|
||||||
|
import ComposePanel from './compose_panel';
|
||||||
|
import NavigationPanel from './navigation_panel';
|
||||||
|
|
||||||
import detectPassiveEvents from 'detect-passive-events';
|
import detectPassiveEvents from 'detect-passive-events';
|
||||||
import { scrollRight } from 'flavours/glitch/util/scroll';
|
import { scrollRight } from 'flavours/glitch/util/scroll';
|
||||||
@@ -49,6 +51,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||||||
swipeToChangeColumns: PropTypes.bool,
|
swipeToChangeColumns: PropTypes.bool,
|
||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
navbarUnder: PropTypes.bool,
|
||||||
|
openSettings: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -108,6 +112,11 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||||||
// React-router does this for us, but too late, feeling laggy.
|
// React-router does this for us, but too late, feeling laggy.
|
||||||
document.querySelector(currentLinkSelector).classList.remove('active');
|
document.querySelector(currentLinkSelector).classList.remove('active');
|
||||||
document.querySelector(nextLinkSelector).classList.add('active');
|
document.querySelector(nextLinkSelector).classList.add('active');
|
||||||
|
|
||||||
|
if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') {
|
||||||
|
this.context.router.history.push(getLink(this.pendingIndex));
|
||||||
|
this.pendingIndex = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAnimationEnd = () => {
|
handleAnimationEnd = () => {
|
||||||
@@ -139,7 +148,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||||||
<ColumnLoading title={title} icon={icon} />;
|
<ColumnLoading title={title} icon={icon} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='columns-area' key={index}>
|
<div className='columns-area columns-area--mobile' key={index}>
|
||||||
{view}
|
{view}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -154,26 +163,45 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { columns, children, singleColumn, swipeToChangeColumns, intl } = this.props;
|
const { columns, children, singleColumn, swipeToChangeColumns, intl, navbarUnder, openSettings } = this.props;
|
||||||
const { shouldAnimate } = this.state;
|
const { shouldAnimate } = this.state;
|
||||||
|
|
||||||
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
||||||
this.pendingIndex = null;
|
|
||||||
|
|
||||||
if (singleColumn) {
|
if (singleColumn) {
|
||||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>;
|
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>;
|
||||||
|
|
||||||
return columnIndex !== -1 ? [
|
const content = columnIndex !== -1 ? (
|
||||||
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={!swipeToChangeColumns}>
|
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={!swipeToChangeColumns}>
|
||||||
{links.map(this.renderView)}
|
{links.map(this.renderView)}
|
||||||
</ReactSwipeableViews>,
|
</ReactSwipeableViews>
|
||||||
|
) : (
|
||||||
|
<div key='content' className='columns-area columns-area--mobile'>{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
floatingActionButton,
|
return (
|
||||||
] : [
|
<div className='columns-area__panels'>
|
||||||
<div className='columns-area'>{children}</div>,
|
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
||||||
|
<div className='columns-area__panels__pane__inner'>
|
||||||
|
<ComposePanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
floatingActionButton,
|
<div className='columns-area__panels__main'>
|
||||||
];
|
{!navbarUnder && <TabsBar key='tabs' />}
|
||||||
|
{content}
|
||||||
|
{navbarUnder && <TabsBar key='tabs' />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
|
||||||
|
<div className='columns-area__panels__pane__inner'>
|
||||||
|
<NavigationPanel onOpenSettings={openSettings} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{floatingActionButton}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import SearchContainer from 'flavours/glitch/features/compose/containers/search_container';
|
||||||
|
import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container';
|
||||||
|
import NavigationContainer from 'flavours/glitch/features/compose/containers/navigation_container';
|
||||||
|
import LinkFooter from './link_footer';
|
||||||
|
|
||||||
|
const ComposePanel = () => (
|
||||||
|
<div className='compose-panel'>
|
||||||
|
<SearchContainer openInRoute />
|
||||||
|
<NavigationContainer />
|
||||||
|
<ComposeFormContainer singleColumn />
|
||||||
|
<LinkFooter withHotkeys />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ComposePanel;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
|
import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
|
||||||
|
import { me } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
locked: state.getIn(['accounts', me, 'locked']),
|
||||||
|
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @withRouter
|
||||||
|
@connect(mapStateToProps)
|
||||||
|
class FollowRequestsNavLink extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
locked: PropTypes.bool,
|
||||||
|
count: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch, locked } = this.props;
|
||||||
|
|
||||||
|
if (locked) {
|
||||||
|
dispatch(fetchFollowRequests());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { locked, count } = this.props;
|
||||||
|
|
||||||
|
if (!locked || count === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NavLink className='column-link column-link--transparent' to='/follow_requests'><IconWithBadge className='column-link__icon' id='user-plus' count={count} /><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></NavLink>;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { invitesEnabled, version, repository, source_url } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { signOutLink } from 'flavours/glitch/util/backend_links';
|
||||||
|
|
||||||
|
const LinkFooter = () => (
|
||||||
|
<div className='getting-started__footer'>
|
||||||
|
<ul>
|
||||||
|
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
|
||||||
|
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
|
||||||
|
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
|
||||||
|
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
|
||||||
|
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
|
||||||
|
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
|
||||||
|
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
|
||||||
|
<li><a href={signOutLink} data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></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>
|
||||||
|
);
|
||||||
|
|
||||||
|
LinkFooter.propTypes = {
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinkFooter;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { fetchLists } from 'flavours/glitch/actions/lists';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
|
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||||
|
if (!lists) {
|
||||||
|
return lists;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
lists: getOrderedLists(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @withRouter
|
||||||
|
@connect(mapStateToProps)
|
||||||
|
class ListPanel extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
lists: ImmutablePropTypes.list,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchLists());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { lists } = this.props;
|
||||||
|
|
||||||
|
if (!lists || lists.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
{lists.map(list => (
|
||||||
|
<NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' icon='list-ul' fixedWidth />{list.get('title')}</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
import { profile_directory } from 'flavours/glitch/util/initial_state';
|
||||||
|
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||||
|
import FollowRequestsNavLink from './follow_requests_nav_link';
|
||||||
|
import ListPanel from './list_panel';
|
||||||
|
|
||||||
|
const NavigationPanel = ({ onOpenSettings }) => (
|
||||||
|
<div className='navigation-panel'>
|
||||||
|
<NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' icon='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
|
||||||
|
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
|
||||||
|
<FollowRequestsNavLink />
|
||||||
|
<NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' icon='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
|
||||||
|
<NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' icon='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
|
||||||
|
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' icon='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
||||||
|
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' icon='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
|
||||||
|
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' icon='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
||||||
|
|
||||||
|
<ListPanel />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<a className='column-link column-link--transparent' href='/settings/preferences' target='_blank'><Icon className='column-link__icon' icon='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
|
||||||
|
<a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' icon='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a>
|
||||||
|
<a className='column-link column-link--transparent' href='/relationships' target='_blank'><Icon className='column-link__icon' icon='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
|
||||||
|
{!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default withRouter(NavigationPanel);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
count: state.getIn(['local_settings', 'notifications', 'tab_badge']) ? state.getIn(['notifications', 'unread']) : 0,
|
||||||
|
id: 'bell',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(IconWithBadge);
|
||||||
@@ -4,40 +4,16 @@ import { NavLink, withRouter } from 'react-router-dom';
|
|||||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { isUserTouching } from 'flavours/glitch/util/is_mobile';
|
import { isUserTouching } from 'flavours/glitch/util/is_mobile';
|
||||||
import { connect } from 'react-redux';
|
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
unreadNotifications: state.getIn(['notifications', 'unread']),
|
|
||||||
showBadge: state.getIn(['local_settings', 'notifications', 'tab_badge']),
|
|
||||||
});
|
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
|
||||||
class NotificationsIcon extends React.PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
unreadNotifications: PropTypes.number,
|
|
||||||
showBadge: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { unreadNotifications, showBadge } = this.props;
|
|
||||||
return (
|
|
||||||
<span className='icon-badge-wrapper'>
|
|
||||||
<i className='fa fa-fw fa-bell' />
|
|
||||||
{ showBadge && unreadNotifications > 0 && <div className='icon-badge' />}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const links = [
|
export const links = [
|
||||||
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
<NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
|
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
|
||||||
|
|
||||||
<NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
|
<NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
|
<NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><i className='fa fa-fw fa-search' /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
|
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><i className='fa fa-fw fa-search' /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
|
||||||
|
<NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><i className='fa fa-fw fa-bars' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><i className='fa fa-fw fa-bars' /></NavLink>,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getIndex (path) {
|
export function getIndex (path) {
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ColumnsArea from '../components/columns_area';
|
import ColumnsArea from '../components/columns_area';
|
||||||
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
columns: state.getIn(['settings', 'columns']),
|
columns: state.getIn(['settings', 'columns']),
|
||||||
swipeToChangeColumns: state.getIn(['local_settings', 'swipe_to_change_columns']),
|
swipeToChangeColumns: state.getIn(['local_settings', 'swipe_to_change_columns']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null, null, { forwardRef: true })(ColumnsArea);
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
openSettings (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dispatch(openModal('SETTINGS', {}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(ColumnsArea);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import NotificationsContainer from './containers/notifications_container';
|
import NotificationsContainer from './containers/notifications_container';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import LoadingBarContainer from './containers/loading_bar_container';
|
import LoadingBarContainer from './containers/loading_bar_container';
|
||||||
import TabsBar from './components/tabs_bar';
|
|
||||||
import ModalContainer from './containers/modal_container';
|
import ModalContainer from './containers/modal_container';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Redirect, withRouter } from 'react-router-dom';
|
import { Redirect, withRouter } from 'react-router-dom';
|
||||||
@@ -45,6 +44,7 @@ import {
|
|||||||
Mutes,
|
Mutes,
|
||||||
PinnedStatuses,
|
PinnedStatuses,
|
||||||
Lists,
|
Lists,
|
||||||
|
Search,
|
||||||
GettingStartedMisc,
|
GettingStartedMisc,
|
||||||
} from 'flavours/glitch/util/async-components';
|
} from 'flavours/glitch/util/async-components';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
@@ -270,19 +270,6 @@ export default class UI extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps) {
|
|
||||||
if (nextProps.navbarUnder !== this.props.navbarUnder) {
|
|
||||||
// Avoid expensive update just to toggle a class
|
|
||||||
this.node.classList.toggle('navbar-under', nextProps.navbarUnder);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Why isn't this working?!?
|
|
||||||
// return super.shouldComponentUpdate(nextProps, nextState);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
|
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
|
||||||
this.columnsAreaNode.handleChildrenContentChange();
|
this.columnsAreaNode.handleChildrenContentChange();
|
||||||
@@ -320,7 +307,7 @@ export default class UI extends React.Component {
|
|||||||
handleHotkeyNew = e => {
|
handleHotkeyNew = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const element = this.node.querySelector('.composer--textarea textarea');
|
const element = this.node.querySelector('.compose-form__autosuggest-wrapper textarea');
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
element.focus();
|
element.focus();
|
||||||
@@ -330,7 +317,7 @@ export default class UI extends React.Component {
|
|||||||
handleHotkeySearch = e => {
|
handleHotkeySearch = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const element = this.node.querySelector('.drawer--search input');
|
const element = this.node.querySelector('.search__input');
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
element.focus();
|
element.focus();
|
||||||
@@ -432,6 +419,8 @@ export default class UI extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
const { width, draggingOver } = this.state;
|
const { width, draggingOver } = this.state;
|
||||||
const { children, layout, isWide, navbarUnder, dropdownMenuIsOpen } = this.props;
|
const { children, layout, isWide, navbarUnder, dropdownMenuIsOpen } = this.props;
|
||||||
|
const singleColumn = isMobile(width, layout);
|
||||||
|
const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
|
||||||
|
|
||||||
const columnsClass = layout => {
|
const columnsClass = layout => {
|
||||||
switch (layout) {
|
switch (layout) {
|
||||||
@@ -475,11 +464,9 @@ export default class UI extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
|
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
|
||||||
<div className={className} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
|
<div className={className} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
|
||||||
{navbarUnder ? null : (<TabsBar />)}
|
<ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={singleColumn} navbarUnder={navbarUnder}>
|
||||||
|
|
||||||
<ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}>
|
|
||||||
<WrappedSwitch>
|
<WrappedSwitch>
|
||||||
<Redirect from='/' to='/getting-started' exact />
|
{redirect}
|
||||||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
||||||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||||
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
|
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
|
||||||
@@ -493,7 +480,7 @@ export default class UI extends React.Component {
|
|||||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
||||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} />
|
<WrappedRoute path='/search' component={Search} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
|
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
|
||||||
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
|
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
|
||||||
@@ -518,7 +505,6 @@ export default class UI extends React.Component {
|
|||||||
</ColumnsAreaContainer>
|
</ColumnsAreaContainer>
|
||||||
|
|
||||||
<NotificationsContainer />
|
<NotificationsContainer />
|
||||||
{navbarUnder ? (<TabsBar />) : null}
|
|
||||||
<LoadingBarContainer className='loading-bar' />
|
<LoadingBarContainer className='loading-bar' />
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { fromJS, is } from 'immutable';
|
|||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen';
|
import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen';
|
||||||
import { displayMedia } from 'flavours/glitch/util/initial_state';
|
import { displayMedia, useBlurhash } from 'flavours/glitch/util/initial_state';
|
||||||
import { decode } from 'blurhash';
|
import { decode } from 'blurhash';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -312,7 +312,7 @@ export default class Video extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_decode () {
|
_decode () {
|
||||||
if (!this.canvas) return;
|
if (!this.canvas || !useBlurhash) return;
|
||||||
|
|
||||||
const hash = this.props.blurhash;
|
const hash = this.props.blurhash;
|
||||||
const pixels = decode(hash, 32, 32);
|
const pixels = decode(hash, 32, 32);
|
||||||
|
|||||||
@@ -442,6 +442,7 @@ export default function compose(state = initialState, action) {
|
|||||||
map.set('focusDate', new Date());
|
map.set('focusDate', new Date());
|
||||||
map.set('caretPosition', null);
|
map.set('caretPosition', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
map.set('sensitive', action.status.get('sensitive'));
|
||||||
|
|
||||||
if (action.status.get('spoiler_text').length > 0) {
|
if (action.status.get('spoiler_text').length > 0) {
|
||||||
map.set('spoiler', true);
|
map.set('spoiler', true);
|
||||||
|
|||||||
102
app/javascript/flavours/glitch/reducers/conversations.js
Normal file
102
app/javascript/flavours/glitch/reducers/conversations.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
import {
|
||||||
|
CONVERSATIONS_MOUNT,
|
||||||
|
CONVERSATIONS_UNMOUNT,
|
||||||
|
CONVERSATIONS_FETCH_REQUEST,
|
||||||
|
CONVERSATIONS_FETCH_SUCCESS,
|
||||||
|
CONVERSATIONS_FETCH_FAIL,
|
||||||
|
CONVERSATIONS_UPDATE,
|
||||||
|
CONVERSATIONS_READ,
|
||||||
|
} from '../actions/conversations';
|
||||||
|
import compareId from 'flavours/glitch/util/compare_id';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
items: ImmutableList(),
|
||||||
|
isLoading: false,
|
||||||
|
hasMore: true,
|
||||||
|
mounted: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversationToMap = item => ImmutableMap({
|
||||||
|
id: item.id,
|
||||||
|
unread: item.unread,
|
||||||
|
accounts: ImmutableList(item.accounts.map(a => a.id)),
|
||||||
|
last_status: item.last_status ? item.last_status.id : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateConversation = (state, item) => state.update('items', list => {
|
||||||
|
const index = list.findIndex(x => x.get('id') === item.id);
|
||||||
|
const newItem = conversationToMap(item);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return list.unshift(newItem);
|
||||||
|
} else {
|
||||||
|
return list.set(index, newItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandNormalizedConversations = (state, conversations, next, isLoadingRecent) => {
|
||||||
|
let items = ImmutableList(conversations.map(conversationToMap));
|
||||||
|
|
||||||
|
return state.withMutations(mutable => {
|
||||||
|
if (!items.isEmpty()) {
|
||||||
|
mutable.update('items', list => {
|
||||||
|
list = list.map(oldItem => {
|
||||||
|
const newItemIndex = items.findIndex(x => x.get('id') === oldItem.get('id'));
|
||||||
|
|
||||||
|
if (newItemIndex === -1) {
|
||||||
|
return oldItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newItem = items.get(newItemIndex);
|
||||||
|
items = items.delete(newItemIndex);
|
||||||
|
|
||||||
|
return newItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
list = list.concat(items);
|
||||||
|
|
||||||
|
return list.sortBy(x => x.get('last_status'), (a, b) => {
|
||||||
|
if(a === null || b === null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareId(a, b) * -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!next && !isLoadingRecent) {
|
||||||
|
mutable.set('hasMore', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutable.set('isLoading', false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function conversations(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case CONVERSATIONS_FETCH_REQUEST:
|
||||||
|
return state.set('isLoading', true);
|
||||||
|
case CONVERSATIONS_FETCH_FAIL:
|
||||||
|
return state.set('isLoading', false);
|
||||||
|
case CONVERSATIONS_FETCH_SUCCESS:
|
||||||
|
return expandNormalizedConversations(state, action.conversations, action.next, action.isLoadingRecent);
|
||||||
|
case CONVERSATIONS_UPDATE:
|
||||||
|
return updateConversation(state, action.conversation);
|
||||||
|
case CONVERSATIONS_MOUNT:
|
||||||
|
return state.update('mounted', count => count + 1);
|
||||||
|
case CONVERSATIONS_UNMOUNT:
|
||||||
|
return state.update('mounted', count => count - 1);
|
||||||
|
case CONVERSATIONS_READ:
|
||||||
|
return state.update('items', list => list.map(item => {
|
||||||
|
if (item.get('id') === action.id) {
|
||||||
|
return item.set('unread', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}));
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -28,6 +28,7 @@ import lists from './lists';
|
|||||||
import listEditor from './list_editor';
|
import listEditor from './list_editor';
|
||||||
import listAdder from './list_adder';
|
import listAdder from './list_adder';
|
||||||
import filters from './filters';
|
import filters from './filters';
|
||||||
|
import conversations from './conversations';
|
||||||
import suggestions from './suggestions';
|
import suggestions from './suggestions';
|
||||||
import pinnedAccountsEditor from './pinned_accounts_editor';
|
import pinnedAccountsEditor from './pinned_accounts_editor';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
@@ -64,6 +65,7 @@ const reducers = {
|
|||||||
listEditor,
|
listEditor,
|
||||||
listAdder,
|
listAdder,
|
||||||
filters,
|
filters,
|
||||||
|
conversations,
|
||||||
suggestions,
|
suggestions,
|
||||||
pinnedAccountsEditor,
|
pinnedAccountsEditor,
|
||||||
polls,
|
polls,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const initialState = ImmutableMap({
|
|||||||
show_reply_count : false,
|
show_reply_count : false,
|
||||||
always_show_spoilers_field: false,
|
always_show_spoilers_field: false,
|
||||||
confirm_missing_media_description: false,
|
confirm_missing_media_description: false,
|
||||||
|
confirm_boost_missing_media_description: false,
|
||||||
confirm_before_clearing_draft: true,
|
confirm_before_clearing_draft: true,
|
||||||
preselect_on_reply: true,
|
preselect_on_reply: true,
|
||||||
inline_preview_cards: true,
|
inline_preview_cards: true,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import compareId from 'flavours/glitch/util/compare_id';
|
|||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
top: true,
|
top: false,
|
||||||
mounted: 0,
|
mounted: 0,
|
||||||
unread: 0,
|
unread: 0,
|
||||||
lastReadId: '0',
|
lastReadId: '0',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user