Merge branch 'main' of https://github.com/glitch-soc/mastodon into fork
This commit is contained in:
commit
390fd84e20
|
@ -1,255 +1,152 @@
|
|||
version: 2
|
||||
version: 2.1
|
||||
|
||||
aliases:
|
||||
- &defaults
|
||||
orbs:
|
||||
ruby: circleci/ruby@1.2.0
|
||||
node: circleci/node@4.7.0
|
||||
|
||||
executors:
|
||||
default:
|
||||
parameters:
|
||||
ruby-version:
|
||||
type: string
|
||||
docker:
|
||||
- image: circleci/ruby:2.7-buster-node
|
||||
environment: &ruby_environment
|
||||
- image: cimg/ruby:<< parameters.ruby-version >>
|
||||
environment:
|
||||
BUNDLE_JOBS: 3
|
||||
BUNDLE_RETRY: 3
|
||||
BUNDLE_APP_CONFIG: ./.bundle/
|
||||
BUNDLE_PATH: ./vendor/bundle/
|
||||
CONTINUOUS_INTEGRATION: true
|
||||
DB_HOST: localhost
|
||||
DB_USER: root
|
||||
RAILS_ENV: test
|
||||
ALLOW_NOPAM: true
|
||||
CONTINUOUS_INTEGRATION: true
|
||||
DISABLE_SIMPLECOV: true
|
||||
PAM_ENABLED: true
|
||||
PAM_DEFAULT_SERVICE: pam_test
|
||||
PAM_CONTROLLED_SERVICE: pam_test_controlled
|
||||
working_directory: ~/projects/mastodon/
|
||||
RAILS_ENV: test
|
||||
- image: cimg/postgres:14.0
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
- image: circleci/redis:6-alpine
|
||||
|
||||
- &attach_workspace
|
||||
attach_workspace:
|
||||
at: ~/projects/
|
||||
commands:
|
||||
install-system-dependencies:
|
||||
steps:
|
||||
- run:
|
||||
name: Install system dependencies
|
||||
command: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
|
||||
install-ruby-dependencies:
|
||||
parameters:
|
||||
ruby-version:
|
||||
type: string
|
||||
steps:
|
||||
- run:
|
||||
command: |
|
||||
bundle config clean 'true'
|
||||
bundle config frozen 'true'
|
||||
bundle config without 'development production'
|
||||
name: Set bundler settings
|
||||
- ruby/install-deps:
|
||||
bundler-version: '2.2.31'
|
||||
key: ruby<< parameters.ruby-version >>-gems-v1
|
||||
wait-db:
|
||||
steps:
|
||||
- run:
|
||||
command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
|
||||
name: Wait for PostgreSQL and Redis
|
||||
|
||||
- &persist_to_workspace
|
||||
persist_to_workspace:
|
||||
root: ~/projects/
|
||||
paths:
|
||||
- ./mastodon/
|
||||
|
||||
- &restore_ruby_dependencies
|
||||
restore_cache:
|
||||
keys:
|
||||
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
|
||||
- v3-ruby-dependencies-
|
||||
|
||||
- &install_steps
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: cimg/ruby:3.0-node
|
||||
environment:
|
||||
RAILS_ENV: test
|
||||
steps:
|
||||
- checkout
|
||||
- *attach_workspace
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
- v2-node-dependencies-
|
||||
- install-system-dependencies
|
||||
- install-ruby-dependencies:
|
||||
ruby-version: '3.0'
|
||||
- node/install-packages:
|
||||
cache-version: v1
|
||||
pkg-manager: yarn
|
||||
- run:
|
||||
name: Install yarn dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
key: v2-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ./node_modules/
|
||||
- *persist_to_workspace
|
||||
|
||||
- &install_system_dependencies
|
||||
run:
|
||||
name: Install system dependencies
|
||||
command: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
|
||||
|
||||
- &install_ruby_dependencies
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run:
|
||||
name: Set Ruby version
|
||||
command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
|
||||
- *restore_ruby_dependencies
|
||||
- run:
|
||||
name: Set bundler settings
|
||||
command: |
|
||||
bundle config --local clean 'true'
|
||||
bundle config --local deployment 'true'
|
||||
bundle config --local with 'pam_authentication'
|
||||
bundle config --local without 'development production'
|
||||
bundle config --local frozen 'true'
|
||||
bundle config --local path $BUNDLE_PATH
|
||||
- run:
|
||||
name: Install bundler dependencies
|
||||
command: bundle check || (bundle install && bundle clean)
|
||||
- save_cache:
|
||||
key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
paths:
|
||||
- ./.bundle/
|
||||
- ./vendor/bundle/
|
||||
- persist_to_workspace:
|
||||
root: ~/projects/
|
||||
paths:
|
||||
- ./mastodon/.bundle/
|
||||
- ./mastodon/vendor/bundle/
|
||||
|
||||
- &test_steps
|
||||
parallelism: 4
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run:
|
||||
name: Install FFMPEG
|
||||
command: sudo apt-get install -y ffmpeg
|
||||
- run:
|
||||
name: Load database schema
|
||||
command: ./bin/rails db:create db:schema:load db:seed
|
||||
- run:
|
||||
name: Run rspec in parallel
|
||||
command: |
|
||||
bundle exec rspec --profile 10 \
|
||||
--format RspecJunitFormatter \
|
||||
--out test_results/rspec.xml \
|
||||
--format progress \
|
||||
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
||||
- store_test_results:
|
||||
path: test_results
|
||||
jobs:
|
||||
install:
|
||||
<<: *defaults
|
||||
<<: *install_steps
|
||||
|
||||
install-ruby2.7:
|
||||
<<: *defaults
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
install-ruby2.6:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.6-buster-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
install-ruby3.0:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:3.0-buster-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run:
|
||||
name: Precompile assets
|
||||
command: ./bin/rails assets:precompile
|
||||
name: Precompile assets
|
||||
- persist_to_workspace:
|
||||
root: ~/projects/
|
||||
paths:
|
||||
- ./mastodon/public/assets
|
||||
- ./mastodon/public/packs-test/
|
||||
- public/assets
|
||||
- public/packs-test
|
||||
root: .
|
||||
|
||||
test:
|
||||
parameters:
|
||||
ruby-version:
|
||||
type: string
|
||||
executor:
|
||||
name: default
|
||||
ruby-version: << parameters.ruby-version >>
|
||||
environment:
|
||||
ALLOW_NOPAM: true
|
||||
PAM_ENABLED: true
|
||||
PAM_DEFAULT_SERVICE: pam_test
|
||||
PAM_CONTROLLED_SERVICE: pam_test_controlled
|
||||
parallelism: 4
|
||||
steps:
|
||||
- checkout
|
||||
- install-system-dependencies
|
||||
- run:
|
||||
command: sudo apt-get install -y ffmpeg imagemagick libpam-dev
|
||||
name: Install additional system dependencies
|
||||
- run:
|
||||
command: bundle config with 'pam_authentication'
|
||||
name: Enable PAM authentication
|
||||
- install-ruby-dependencies:
|
||||
ruby-version: << parameters.ruby-version >>
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- wait-db
|
||||
- run:
|
||||
command: ./bin/rails db:create db:schema:load db:seed
|
||||
name: Load database schema
|
||||
- ruby/rspec-test
|
||||
|
||||
test-migrations:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.7-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:12.2
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
- image: circleci/redis:5-alpine
|
||||
executor:
|
||||
name: default
|
||||
ruby-version: '3.0'
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- checkout
|
||||
- install-system-dependencies
|
||||
- install-ruby-dependencies:
|
||||
ruby-version: '3.0'
|
||||
- wait-db
|
||||
- run:
|
||||
name: Create database
|
||||
command: ./bin/rails db:create
|
||||
name: Create database
|
||||
- run:
|
||||
name: Run migrations
|
||||
command: ./bin/rails db:migrate
|
||||
|
||||
test-ruby2.7:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.7-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:12.2
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-ruby2.6:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.6-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:12.2
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-ruby3.0:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:3.0-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:12.2
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-webui:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/node:14-buster
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run:
|
||||
name: Run jest
|
||||
command: yarn test:jest
|
||||
name: Run migrations
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-and-test:
|
||||
jobs:
|
||||
- install
|
||||
- install-ruby2.7:
|
||||
- build
|
||||
- test:
|
||||
matrix:
|
||||
parameters:
|
||||
ruby-version:
|
||||
- '2.7'
|
||||
- '3.0'
|
||||
name: test-ruby<< matrix.ruby-version >>
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.6:
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.7
|
||||
- install-ruby3.0:
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.7
|
||||
- build:
|
||||
requires:
|
||||
- install-ruby2.7
|
||||
- build
|
||||
- test-migrations:
|
||||
requires:
|
||||
- install-ruby2.7
|
||||
- test-ruby2.7:
|
||||
requires:
|
||||
- install-ruby2.7
|
||||
- build
|
||||
- test-ruby2.6:
|
||||
- node/run:
|
||||
cache-version: v1
|
||||
name: test-webui
|
||||
pkg-manager: yarn
|
||||
requires:
|
||||
- install-ruby2.6
|
||||
- build
|
||||
- test-ruby3.0:
|
||||
requires:
|
||||
- install-ruby3.0
|
||||
- build
|
||||
- test-webui:
|
||||
requires:
|
||||
- install
|
||||
version: lts
|
||||
yarn-run: test:jest
|
||||
|
|
|
@ -35,4 +35,7 @@ plugins:
|
|||
enabled: true
|
||||
exclude_patterns:
|
||||
- spec/
|
||||
- vendor/asset
|
||||
- vendor/asset/
|
||||
|
||||
- app/javascript/mastodon/locales/**/*.json
|
||||
- config/locales/**/*.yml
|
||||
|
|
|
@ -13,7 +13,7 @@ DB_PORT=5432
|
|||
|
||||
# DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
|
||||
|
||||
# Optional ElasticSearch configuration
|
||||
# Optional Elasticsearch configuration
|
||||
ES_ENABLED=true
|
||||
ES_HOST=$DATA_ELASTIC_HOST
|
||||
ES_PORT=9200
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
# not demonstrate all available configuration options. Please look at
|
||||
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
|
||||
|
||||
# Note that this file accepts slightly different syntax depending on whether
|
||||
# you are using `docker-compose` or not. In particular, if you use
|
||||
# `docker-compose`, the value of each declared variable will be taken verbatim,
|
||||
# including surrounding quotes.
|
||||
# See: https://github.com/mastodon/mastodon/issues/16895
|
||||
|
||||
# Federation
|
||||
# ----------
|
||||
# This identifies your server and cannot be changed safely later
|
||||
|
@ -50,11 +56,14 @@ DB_PASS=
|
|||
DB_PORT=5432
|
||||
|
||||
|
||||
# ElasticSearch (optional)
|
||||
# Elasticsearch (optional)
|
||||
# ------------------------
|
||||
#ES_ENABLED=true
|
||||
#ES_HOST=localhost
|
||||
#ES_PORT=9200
|
||||
# Authentication for ES (optional)
|
||||
#ES_USER=elastic
|
||||
#ES_PASS=password
|
||||
|
||||
|
||||
# Secrets
|
||||
|
|
|
@ -8,6 +8,17 @@ body:
|
|||
Make sure that you are submitting a new bug that was not previously reported or already fixed.
|
||||
|
||||
Please use a concise and distinct title for the issue.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce the problem
|
||||
description: What were you trying to do?
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
|
@ -20,17 +31,6 @@ body:
|
|||
description: What happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce the problem
|
||||
description: What were you trying to do?
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Specifications
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
name: Build container image
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
tags:
|
||||
- "*"
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
images: tootsuite/mastodon
|
||||
flavor: |
|
||||
latest=auto
|
||||
tags: |
|
||||
type=edge,branch=main
|
||||
type=semver,pattern={{ raw }}
|
||||
- uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=registry,ref=tootsuite/mastodon:latest
|
||||
cache-to: type=inline
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
ruby-version: '3.0'
|
||||
bundler-cache: true
|
||||
- name: Check locale file normalization
|
||||
run: bundle exec i18n-tasks check-normalized
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.7.4
|
||||
3.0.3
|
||||
|
|
48
CHANGELOG.md
48
CHANGELOG.md
|
@ -3,6 +3,54 @@ Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [3.4.3] - 2021-11-06
|
||||
### Fixed
|
||||
|
||||
- Fix login being broken due to inaccurately applied backport fix in 3.4.2 ([Gargron](https://github.com/mastodon/mastodon/commit/5c47a18c8df3231aa25c6d1f140a71a7fac9cbf9))
|
||||
|
||||
## [3.4.2] - 2021-11-06
|
||||
### Added
|
||||
|
||||
- Add `configuration` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/16485))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix handling of back button with modal windows in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16499))
|
||||
- Fix pop-in player when author has long username in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16468))
|
||||
- Fix crash when a status with a playing video gets deleted in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16384))
|
||||
- Fix crash with Microsoft Translate in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16525))
|
||||
- Fix PWA not being usable from alternate domains ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16714))
|
||||
- Fix locale-specific number rounding errors ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16469))
|
||||
- Fix scheduling a status decreasing status count ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16791))
|
||||
- Fix user's canonical email address being blocked when user deletes own account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16503))
|
||||
- Fix not being able to suspend users that already have their canonical e-mail blocked ([Gargron](https://github.com/mastodon/mastodon/pull/16455))
|
||||
- Fix anonymous access to outbox not being cached by the reverse proxy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16458))
|
||||
- Fix followers synchronization mechanism not working when URI has empty path ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16744))
|
||||
- Fix serialization of counts in REST API when user hides their network ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16418))
|
||||
- Fix inefficiencies in auto-linking code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16506))
|
||||
- Fix `tootctl self-destruct` not sending delete activities for recently-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16688))
|
||||
- Fix suspicious sign-in e-mail text being out of date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16690))
|
||||
- Fix some frameworks being unnecessarily loaded ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16725))
|
||||
- Fix canonical e-mail blocks missing foreign key constraints ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16448))
|
||||
- Fix inconsistent order on account's statuses page in admin UI ([tribela](https://github.com/mastodon/mastodon/pull/16937))
|
||||
- Fix media from blocked domains being redownloaded by `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/16914))
|
||||
- Fix `mastodon:setup` generated env-file syntax ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16896))
|
||||
- Fix link previews being incorrectly generated from earlier links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16885))
|
||||
- Fix wrong `to`/`cc` values for remote groups in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16700))
|
||||
- Fix mentions with non-ascii TLDs not being processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16689))
|
||||
- Fix authentication failures halfway through a sign-in attempt ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16792))
|
||||
- Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628))
|
||||
- Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598))
|
||||
- Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583))
|
||||
- Fix newlines being added to accout notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
|
||||
- Fix crash when creating an announcement with links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16941))
|
||||
- Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix user notes not having a length limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16942))
|
||||
- Fix revoking a specific session not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
|
||||
|
||||
## [3.4.1] - 2021-06-03
|
||||
### Added
|
||||
|
||||
|
|
18
Dockerfile
18
Dockerfile
|
@ -2,9 +2,10 @@ FROM ubuntu:20.04 as build-dep
|
|||
|
||||
# Use bash for the shell
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
|
||||
# Install Node v14 (LTS)
|
||||
ENV NODE_VER="14.17.6"
|
||||
# Install Node v16 (LTS)
|
||||
ENV NODE_VER="16.13.0"
|
||||
RUN ARCH= && \
|
||||
dpkgArch="$(dpkg --print-architecture)" && \
|
||||
case "${dpkgArch##*-}" in \
|
||||
|
@ -18,15 +19,15 @@ RUN ARCH= && \
|
|||
esac && \
|
||||
echo "Etc/UTC" > /etc/localtime && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates wget python && \
|
||||
apt-get install -y --no-install-recommends ca-certificates wget python apt-utils && \
|
||||
cd ~ && \
|
||||
wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||
tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
||||
|
||||
# Install Ruby
|
||||
ENV RUBY_VER="2.7.4"
|
||||
# Install Ruby 3.0
|
||||
ENV RUBY_VER="3.0.3"
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends build-essential \
|
||||
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
||||
|
@ -45,7 +46,8 @@ RUN apt-get update && \
|
|||
|
||||
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
|
||||
|
||||
RUN npm install -g yarn && \
|
||||
RUN npm install -g npm@latest && \
|
||||
npm install -g yarn && \
|
||||
gem install bundler && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
|
||||
|
@ -56,6 +58,7 @@ COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
|||
RUN cd /opt/mastodon && \
|
||||
bundle config set deployment 'true' && \
|
||||
bundle config set without 'development test' && \
|
||||
bundle config set silence_root_warning true && \
|
||||
bundle install -j"$(nproc)" && \
|
||||
yarn install --pure-lockfile
|
||||
|
||||
|
@ -81,11 +84,12 @@ RUN apt-get update && \
|
|||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install mastodon runtime deps
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
|
||||
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
|
||||
file ca-certificates tzdata libreadline8 gcc tini && \
|
||||
file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
|
||||
ln -s /opt/mastodon /mastodon && \
|
||||
gem install bundler && \
|
||||
rm -rf /var/cache && \
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
## ActivityPub federation in Mastodon
|
||||
|
||||
Mastodon largely follows the ActivityPub server-to-server specification but it makes uses of some non-standard extensions, some of which are required for interacting with Mastodon at all.
|
||||
|
||||
Supported vocabulary: https://docs.joinmastodon.org/spec/activitypub/
|
||||
|
||||
### Required extensions
|
||||
|
||||
#### Webfinger
|
||||
|
||||
In Mastodon, users are identified by a `username` and `domain` pair (e.g., `Gargron@mastodon.social`).
|
||||
This is used both for discovery and for unambiguously mentioning users across the fediverse. Furthermore, this is part of Mastodon's database design from its very beginnings.
|
||||
|
||||
As a result, Mastodon requires that each ActivityPub actor uniquely maps back to an `acct:` URI that can be resolved via WebFinger.
|
||||
|
||||
More information and examples are available at: https://docs.joinmastodon.org/spec/webfinger/
|
||||
|
||||
#### HTTP Signatures
|
||||
|
||||
In order to authenticate activities, Mastodon relies on HTTP Signatures, signing every `POST` and `GET` request to other ActivityPub implementations on behalf of the user authoring an activity (for `POST` requests) or an actor representing the Mastodon server itself (for most `GET` requests).
|
||||
|
||||
Mastodon requires all `POST` requests to be signed, and MAY require `GET` requests to be signed, depending on the configuration of the Mastodon server.
|
||||
|
||||
More information on HTTP Signatures, as well as examples, can be found here: https://docs.joinmastodon.org/spec/security/#http
|
||||
|
||||
### Optional extensions
|
||||
|
||||
- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld
|
||||
- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/
|
||||
- Followers collection synchronization: https://git.activitypub.dev/ActivityPubDev/Fediverse-Enhancement-Proposals/src/branch/main/feps/fep-8fcf.md
|
29
Gemfile
29
Gemfile
|
@ -4,8 +4,9 @@ source 'https://rubygems.org'
|
|||
ruby '>= 2.5.0', '< 3.1.0'
|
||||
|
||||
gem 'pkg-config', '~> 1.4'
|
||||
gem 'rexml', '~> 3.2'
|
||||
|
||||
gem 'puma', '~> 5.4'
|
||||
gem 'puma', '~> 5.5'
|
||||
gem 'rails', '~> 6.1.4'
|
||||
gem 'sprockets', '~> 3.7.2'
|
||||
gem 'thor', '~> 1.1'
|
||||
|
@ -17,20 +18,20 @@ gem 'makara', '~> 0.5'
|
|||
gem 'pghero', '~> 2.8'
|
||||
gem 'dotenv-rails', '~> 2.7'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.102', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.107', require: false
|
||||
gem 'fog-core', '<= 2.1.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
gem 'kt-paperclip', '~> 7.0'
|
||||
gem 'blurhash', '~> 0.1'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.10'
|
||||
gem 'addressable', '~> 2.8'
|
||||
gem 'bootsnap', '~> 1.8.1', require: false
|
||||
gem 'bootsnap', '~> 1.9.2', require: false
|
||||
gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.7'
|
||||
gem 'iso-639'
|
||||
gem 'chewy', '~> 5.2'
|
||||
gem 'cld3', '~> 3.4.2'
|
||||
gem 'chewy', '~> 7.2'
|
||||
gem 'cld3', '~> 3.4.3'
|
||||
gem 'devise', '~> 4.8'
|
||||
gem 'devise-two-factor', '~> 4.0'
|
||||
|
||||
|
@ -59,13 +60,12 @@ gem 'httplog', '~> 1.5.0'
|
|||
gem 'idn-ruby', require: 'idn'
|
||||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
||||
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
|
||||
gem 'nokogiri', '~> 1.12'
|
||||
gem 'nsa', '~> 0.2'
|
||||
gem 'oj', '~> 3.13'
|
||||
gem 'ox', '~> 2.14'
|
||||
gem 'parslet'
|
||||
gem 'parallel', '~> 1.21'
|
||||
gem 'posix-spawn'
|
||||
gem 'pundit', '~> 2.1'
|
||||
gem 'premailer-rails'
|
||||
|
@ -73,19 +73,19 @@ gem 'rack-attack', '~> 6.5'
|
|||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 6.0'
|
||||
gem 'rails-settings-cached', '~> 0.6'
|
||||
gem 'redis', '~> 4.4', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||
gem 'rqrcode', '~> 2.1'
|
||||
gem 'ruby-progressbar', '~> 1.11'
|
||||
gem 'sanitize', '~> 6.0'
|
||||
gem 'scenic', '~> 1.5'
|
||||
gem 'sidekiq', '~> 6.2'
|
||||
gem 'sidekiq', '~> 6.3'
|
||||
gem 'sidekiq-scheduler', '~> 3.1'
|
||||
gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||
gem 'sidekiq-bulk', '~>0.2.0'
|
||||
gem 'simple-navigation', '~> 4.3'
|
||||
gem 'simple_form', '~> 5.1'
|
||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
|
||||
gem 'stoplight', '~> 2.2.1'
|
||||
gem 'strong_migrations', '~> 0.7'
|
||||
gem 'tty-prompt', '~> 0.23', require: false
|
||||
|
@ -115,7 +115,7 @@ group :production, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.35'
|
||||
gem 'capybara', '~> 3.36'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.19'
|
||||
gem 'microformats', '~> 4.2'
|
||||
|
@ -123,7 +123,6 @@ group :test do
|
|||
gem 'rspec-sidekiq', '~> 3.1'
|
||||
gem 'simplecov', '~> 0.21', require: false
|
||||
gem 'webmock', '~> 3.14'
|
||||
gem 'parallel_tests', '~> 3.7'
|
||||
gem 'rspec_junit_formatter', '~> 0.4'
|
||||
end
|
||||
|
||||
|
@ -134,9 +133,9 @@ group :development do
|
|||
gem 'binding_of_caller', '~> 1.0'
|
||||
gem 'bullet', '~> 6.1'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.4'
|
||||
gem 'letter_opener_web', '~> 2.0'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 1.21', require: false
|
||||
gem 'rubocop', '~> 1.23', require: false
|
||||
gem 'rubocop-rails', '~> 2.12', require: false
|
||||
gem 'brakeman', '~> 5.1', require: false
|
||||
gem 'bundler-audit', '~> 0.9', require: false
|
||||
|
|
181
Gemfile.lock
181
Gemfile.lock
|
@ -79,17 +79,17 @@ GEM
|
|||
encryptor (~> 3.0.0)
|
||||
awrence (1.1.1)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.496.0)
|
||||
aws-sdk-core (3.121.0)
|
||||
aws-partitions (1.534.0)
|
||||
aws-sdk-core (3.123.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.48.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-kms (1.51.0)
|
||||
aws-sdk-core (~> 3, >= 3.122.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.102.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-s3 (1.107.0)
|
||||
aws-sdk-core (~> 3, >= 3.122.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
|
@ -104,9 +104,9 @@ GEM
|
|||
debug_inspector (>= 0.0.1)
|
||||
blurhash (0.1.5)
|
||||
ffi (~> 1.14)
|
||||
bootsnap (1.8.1)
|
||||
bootsnap (1.9.3)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (5.1.1)
|
||||
brakeman (5.1.2)
|
||||
browser (4.2.0)
|
||||
brpoplpush-redis_script (0.1.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
|
@ -134,8 +134,9 @@ GEM
|
|||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (3.35.3)
|
||||
capybara (3.36.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
rack (>= 1.6.0)
|
||||
|
@ -146,12 +147,12 @@ GEM
|
|||
activesupport
|
||||
cbor (0.5.9.6)
|
||||
charlock_holmes (0.7.7)
|
||||
chewy (5.2.0)
|
||||
chewy (7.2.3)
|
||||
activesupport (>= 5.2)
|
||||
elasticsearch (>= 2.0.0)
|
||||
elasticsearch (>= 7.12.0, < 7.14.0)
|
||||
elasticsearch-dsl
|
||||
chunky_png (1.4.0)
|
||||
cld3 (3.4.2)
|
||||
cld3 (3.4.3)
|
||||
ffi (>= 1.1.0, < 1.16.0)
|
||||
climate_control (0.2.0)
|
||||
coderay (1.1.3)
|
||||
|
@ -188,7 +189,7 @@ GEM
|
|||
docile (1.3.4)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.5.2)
|
||||
doorkeeper (5.5.4)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.6)
|
||||
dotenv-rails (2.7.6)
|
||||
|
@ -196,13 +197,13 @@ GEM
|
|||
railties (>= 3.2)
|
||||
e2mmap (0.1.0)
|
||||
ed25519 (1.2.4)
|
||||
elasticsearch (7.10.1)
|
||||
elasticsearch-api (= 7.10.1)
|
||||
elasticsearch-transport (= 7.10.1)
|
||||
elasticsearch-api (7.10.1)
|
||||
elasticsearch (7.13.3)
|
||||
elasticsearch-api (= 7.13.3)
|
||||
elasticsearch-transport (= 7.13.3)
|
||||
elasticsearch-api (7.13.3)
|
||||
multi_json
|
||||
elasticsearch-dsl (0.1.9)
|
||||
elasticsearch-transport (7.10.1)
|
||||
elasticsearch-dsl (0.1.10)
|
||||
elasticsearch-transport (7.13.3)
|
||||
faraday (~> 1)
|
||||
multi_json
|
||||
encryptor (3.0.0)
|
||||
|
@ -213,11 +214,25 @@ GEM
|
|||
fabrication (2.22.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.3.0)
|
||||
faraday (1.8.0)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0.1)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.1)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ruby2_keywords
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
fast_blank (1.0.1)
|
||||
fastimage (2.2.5)
|
||||
ffi (1.15.4)
|
||||
|
@ -262,7 +277,7 @@ GEM
|
|||
hiredis (0.6.3)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http (5.0.2)
|
||||
http (5.0.4)
|
||||
addressable (~> 2.8)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
|
@ -274,9 +289,9 @@ GEM
|
|||
httplog (1.5.0)
|
||||
rack (>= 1.0)
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.8.10)
|
||||
i18n (1.8.11)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (0.9.34)
|
||||
i18n-tasks (0.9.35)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
erubi
|
||||
|
@ -286,13 +301,13 @@ GEM
|
|||
rails-i18n
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
terminal-table (>= 1.5.1)
|
||||
idn-ruby (0.1.2)
|
||||
idn-ruby (0.1.4)
|
||||
ipaddress (0.8.3)
|
||||
iso-639 (0.3.5)
|
||||
jmespath (1.4.0)
|
||||
json (2.5.1)
|
||||
json-canonicalization (0.2.1)
|
||||
json-ld (3.1.9)
|
||||
json-ld (3.1.10)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 0.2)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
|
@ -316,14 +331,21 @@ GEM
|
|||
activerecord
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-core (1.2.1)
|
||||
kt-paperclip (7.0.1)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
marcel (~> 1.0.1)
|
||||
mime-types
|
||||
terrapin (~> 0.6.0)
|
||||
launchy (2.5.0)
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
letter_opener_web (1.4.0)
|
||||
actionmailer (>= 3.2)
|
||||
letter_opener (~> 1.0)
|
||||
railties (>= 3.2)
|
||||
letter_opener_web (2.0.0)
|
||||
actionmailer (>= 5.2)
|
||||
letter_opener (~> 1.7)
|
||||
railties (>= 5.2)
|
||||
rexml
|
||||
link_header (0.0.8)
|
||||
llhttp-ffi (0.4.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
|
@ -343,18 +365,16 @@ GEM
|
|||
marcel (1.0.1)
|
||||
mario-redis-lock (1.2.1)
|
||||
redis (>= 3.0.5)
|
||||
matrix (0.4.2)
|
||||
memory_profiler (1.0.0)
|
||||
method_source (1.0.0)
|
||||
microformats (4.3.1)
|
||||
json (~> 2.2)
|
||||
nokogiri (~> 1.10)
|
||||
mime-types (3.3.1)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2020.0512)
|
||||
mimemagic (0.3.10)
|
||||
nokogiri (~> 1)
|
||||
rake
|
||||
mini_mime (1.1.1)
|
||||
mime-types-data (3.2021.1115)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.6.1)
|
||||
minitest (5.14.4)
|
||||
msgpack (1.4.2)
|
||||
|
@ -365,7 +385,7 @@ GEM
|
|||
net-ssh (>= 2.6.5, < 7.0.0)
|
||||
net-ssh (6.1.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.12.4)
|
||||
nokogiri (1.12.5)
|
||||
mini_portile2 (~> 2.6.1)
|
||||
racc (~> 1.4)
|
||||
nsa (0.2.8)
|
||||
|
@ -373,7 +393,7 @@ GEM
|
|||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.13.7)
|
||||
oj (3.13.9)
|
||||
omniauth (1.9.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
|
@ -390,16 +410,8 @@ GEM
|
|||
openssl (2.2.0)
|
||||
openssl-signature_algorithm (0.4.0)
|
||||
orm_adapter (0.5.0)
|
||||
ox (2.14.5)
|
||||
paperclip (6.0.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
mime-types
|
||||
mimemagic (~> 0.3.0)
|
||||
terrapin (~> 0.6.0)
|
||||
ox (2.14.6)
|
||||
parallel (1.21.0)
|
||||
parallel_tests (3.7.1)
|
||||
parallel
|
||||
parser (3.0.2.0)
|
||||
ast (~> 2.4.1)
|
||||
parslet (2.0.0)
|
||||
|
@ -427,12 +439,12 @@ GEM
|
|||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.4.0)
|
||||
puma (5.5.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.1)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.5.2)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3)
|
||||
rack-attack (6.5.0)
|
||||
rack (>= 1.0, < 3)
|
||||
|
@ -485,7 +497,7 @@ GEM
|
|||
rdf-normalize (0.4.0)
|
||||
rdf (~> 3.1)
|
||||
redcarpet (3.5.1)
|
||||
redis (4.4.0)
|
||||
redis (4.5.1)
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.1.1)
|
||||
|
@ -523,18 +535,18 @@ GEM
|
|||
rspec-support (3.10.2)
|
||||
rspec_junit_formatter (0.4.1)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (1.21.0)
|
||||
rubocop (1.23.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.9.1, < 2.0)
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.11.0)
|
||||
rubocop-ast (1.13.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-rails (2.12.2)
|
||||
rubocop-rails (2.12.4)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
|
@ -542,7 +554,7 @@ GEM
|
|||
ruby-saml (1.13.0)
|
||||
nokogiri (>= 1.10.5)
|
||||
rexml
|
||||
ruby2_keywords (0.0.4)
|
||||
ruby2_keywords (0.0.5)
|
||||
rufus-scheduler (3.7.0)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
safety_net_attestation (0.4.0)
|
||||
|
@ -555,7 +567,7 @@ GEM
|
|||
railties (>= 4.0.0)
|
||||
securecompare (1.0.0)
|
||||
semantic_range (3.0.0)
|
||||
sidekiq (6.2.2)
|
||||
sidekiq (6.3.1)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
|
@ -568,11 +580,11 @@ GEM
|
|||
sidekiq (>= 3)
|
||||
thwait
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (7.1.5)
|
||||
sidekiq-unique-jobs (7.1.8)
|
||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 5.0, < 7.0)
|
||||
thor (>= 0.20, < 2.0)
|
||||
sidekiq (>= 5.0, < 8.0)
|
||||
thor (>= 0.20, < 3.0)
|
||||
simple-navigation (4.3.0)
|
||||
activesupport (>= 2.3.2)
|
||||
simple_form (5.1.0)
|
||||
|
@ -587,9 +599,9 @@ GEM
|
|||
sprockets (3.7.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.2)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets-rails (3.4.1)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkit (1.21.2)
|
||||
net-scp (>= 1.1.2)
|
||||
|
@ -600,8 +612,8 @@ GEM
|
|||
strong_migrations (0.7.8)
|
||||
activerecord (>= 5)
|
||||
temple (0.8.2)
|
||||
terminal-table (3.0.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
thor (1.1.0)
|
||||
|
@ -626,12 +638,12 @@ GEM
|
|||
unf (~> 0.1.0)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2021.1)
|
||||
tzinfo-data (1.2021.5)
|
||||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
unf_ext (0.0.8)
|
||||
unicode-display_width (2.1.0)
|
||||
uniform_notifier (1.14.2)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
|
@ -649,7 +661,7 @@ GEM
|
|||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (5.4.2)
|
||||
webpacker (5.4.3)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
|
@ -664,7 +676,7 @@ GEM
|
|||
xorcist (1.1.2)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.4.2)
|
||||
zeitwerk (2.5.1)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -674,11 +686,11 @@ DEPENDENCIES
|
|||
active_record_query_trace (~> 1.8)
|
||||
addressable (~> 2.8)
|
||||
annotate (~> 3.1)
|
||||
aws-sdk-s3 (~> 1.102)
|
||||
aws-sdk-s3 (~> 1.107)
|
||||
better_errors (~> 2.9)
|
||||
binding_of_caller (~> 1.0)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.8.1)
|
||||
bootsnap (~> 1.9.2)
|
||||
brakeman (~> 5.1)
|
||||
browser
|
||||
bullet (~> 6.1)
|
||||
|
@ -687,10 +699,10 @@ DEPENDENCIES
|
|||
capistrano-rails (~> 1.6)
|
||||
capistrano-rbenv (~> 2.2)
|
||||
capistrano-yarn (~> 2.0)
|
||||
capybara (~> 3.35)
|
||||
capybara (~> 3.36)
|
||||
charlock_holmes (~> 0.7.7)
|
||||
chewy (~> 5.2)
|
||||
cld3 (~> 3.4.2)
|
||||
chewy (~> 7.2)
|
||||
cld3 (~> 3.4.3)
|
||||
climate_control (~> 0.2)
|
||||
color_diff (~> 0.1)
|
||||
concurrent-ruby
|
||||
|
@ -721,15 +733,16 @@ DEPENDENCIES
|
|||
json-ld
|
||||
json-ld-preloaded (~> 3.1)
|
||||
kaminari (~> 1.2)
|
||||
kt-paperclip (~> 7.0)
|
||||
letter_opener (~> 1.7)
|
||||
letter_opener_web (~> 1.4)
|
||||
letter_opener_web (~> 2.0)
|
||||
link_header (~> 0.0)
|
||||
lograge (~> 0.11)
|
||||
makara (~> 0.5)
|
||||
mario-redis-lock (~> 1.2)
|
||||
memory_profiler
|
||||
microformats (~> 4.2)
|
||||
mime-types (~> 3.3.1)
|
||||
mime-types (~> 3.4.1)
|
||||
net-ldap (~> 0.17)
|
||||
nokogiri (~> 1.12)
|
||||
nsa (~> 0.2)
|
||||
|
@ -739,9 +752,6 @@ DEPENDENCIES
|
|||
omniauth-rails_csrf_protection (~> 0.1)
|
||||
omniauth-saml (~> 1.10)
|
||||
ox (~> 2.14)
|
||||
paperclip (~> 6.0)
|
||||
parallel (~> 1.21)
|
||||
parallel_tests (~> 3.7)
|
||||
parslet
|
||||
pg (~> 1.2)
|
||||
pghero (~> 2.8)
|
||||
|
@ -751,7 +761,7 @@ DEPENDENCIES
|
|||
private_address_check (~> 0.5)
|
||||
pry-byebug (~> 3.9)
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 5.4)
|
||||
puma (~> 5.5)
|
||||
pundit (~> 2.1)
|
||||
rack (~> 2.2.3)
|
||||
rack-attack (~> 6.5)
|
||||
|
@ -762,18 +772,19 @@ DEPENDENCIES
|
|||
rails-settings-cached (~> 0.6)
|
||||
rdf-normalize (~> 0.4)
|
||||
redcarpet (~> 3.5)
|
||||
redis (~> 4.4)
|
||||
redis (~> 4.5)
|
||||
redis-namespace (~> 1.8)
|
||||
rexml (~> 3.2)
|
||||
rqrcode (~> 2.1)
|
||||
rspec-rails (~> 5.0)
|
||||
rspec-sidekiq (~> 3.1)
|
||||
rspec_junit_formatter (~> 0.4)
|
||||
rubocop (~> 1.21)
|
||||
rubocop (~> 1.23)
|
||||
rubocop-rails (~> 2.12)
|
||||
ruby-progressbar (~> 1.11)
|
||||
sanitize (~> 6.0)
|
||||
scenic (~> 1.5)
|
||||
sidekiq (~> 6.2)
|
||||
sidekiq (~> 6.3)
|
||||
sidekiq-bulk (~> 0.2.0)
|
||||
sidekiq-scheduler (~> 3.1)
|
||||
sidekiq-unique-jobs (~> 7.1)
|
||||
|
@ -781,7 +792,7 @@ DEPENDENCIES
|
|||
simple_form (~> 5.1)
|
||||
simplecov (~> 0.21)
|
||||
sprockets (~> 3.7.2)
|
||||
sprockets-rails (~> 3.2)
|
||||
sprockets-rails (~> 3.4)
|
||||
stackprof
|
||||
stoplight (~> 2.2.1)
|
||||
strong_migrations (~> 0.7)
|
||||
|
|
|
@ -45,16 +45,8 @@ sudo apt-get install \
|
|||
# Install rvm
|
||||
read RUBY_VERSION < .ruby-version
|
||||
|
||||
gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
|
||||
$($gpg_command)
|
||||
if [ $? -ne 0 ];then
|
||||
echo "GPG command failed, This prevented RVM from installing."
|
||||
echo "Retrying once..." && $($gpg_command)
|
||||
if [ $? -ne 0 ];then
|
||||
echo "GPG failed for the second time, please ensure network connectivity."
|
||||
echo "Exiting..." && exit 1
|
||||
fi
|
||||
fi
|
||||
curl -sSL https://rvm.io/mpapis.asc | gpg --import
|
||||
curl -sSL https://rvm.io/pkuczynski.asc | gpg --import
|
||||
|
||||
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
|
||||
source /home/vagrant/.rvm/scripts/rvm
|
||||
|
|
|
@ -23,21 +23,21 @@ class AccountsIndex < Chewy::Index
|
|||
},
|
||||
}
|
||||
|
||||
define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do
|
||||
root date_detection: false do
|
||||
field :id, type: 'long'
|
||||
index_scope ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? }
|
||||
|
||||
field :display_name, type: 'text', analyzer: 'content' do
|
||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||
end
|
||||
root date_detection: false do
|
||||
field :id, type: 'long'
|
||||
|
||||
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
|
||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||
end
|
||||
|
||||
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
|
||||
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
|
||||
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
||||
field :display_name, type: 'text', analyzer: 'content' do
|
||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||
end
|
||||
|
||||
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
|
||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||
end
|
||||
|
||||
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
|
||||
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
|
||||
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,36 +31,36 @@ class StatusesIndex < Chewy::Index
|
|||
},
|
||||
}
|
||||
|
||||
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) do
|
||||
crutch :mentions do |collection|
|
||||
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
|
||||
|
||||
crutch :mentions do |collection|
|
||||
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :favourites do |collection|
|
||||
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :reblogs do |collection|
|
||||
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :bookmarks do |collection|
|
||||
data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
root date_detection: false do
|
||||
field :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)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
|
||||
field :stemmed, type: 'text', analyzer: 'content'
|
||||
end
|
||||
|
||||
crutch :favourites do |collection|
|
||||
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :reblogs do |collection|
|
||||
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :bookmarks do |collection|
|
||||
data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
root date_detection: false do
|
||||
field :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)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
|
||||
field :stemmed, type: 'text', analyzer: 'content'
|
||||
end
|
||||
|
||||
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
||||
end
|
||||
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,15 +23,15 @@ class TagsIndex < Chewy::Index
|
|||
},
|
||||
}
|
||||
|
||||
define_type ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } do
|
||||
root date_detection: false do
|
||||
field :name, type: 'text', analyzer: 'content' do
|
||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||
end
|
||||
index_scope ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? }
|
||||
|
||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } }
|
||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
||||
root date_detection: false do
|
||||
field :name, type: 'text', analyzer: 'content' do
|
||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||
end
|
||||
|
||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day.accounts } }
|
||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,50 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
require 'sidekiq/api'
|
||||
|
||||
module Admin
|
||||
class DashboardController < BaseController
|
||||
def index
|
||||
@system_checks = Admin::SystemCheck.perform
|
||||
@users_count = User.count
|
||||
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
|
||||
@pending_users_count = User.pending.count
|
||||
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
|
||||
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
|
||||
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
|
||||
@relay_enabled = Relay.enabled.exists?
|
||||
@single_user_mode = Rails.configuration.x.single_user_mode
|
||||
@registrations_enabled = Setting.registrations_mode != 'none'
|
||||
@deletions_enabled = Setting.open_deletion
|
||||
@invites_enabled = Setting.min_invite_role == 'user'
|
||||
@search_enabled = Chewy.enabled?
|
||||
@version = Mastodon::Version.to_s
|
||||
@database_version = ActiveRecord::Base.connection.execute('SELECT VERSION()').first['version'].match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
|
||||
@redis_version = redis_info['redis_version']
|
||||
@reports_count = Report.unresolved.count
|
||||
@queue_backlog = Sidekiq::Stats.new.enqueued
|
||||
@recent_users = User.confirmed.recent.includes(:account).limit(8)
|
||||
@database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
|
||||
@redis_size = redis_info['used_memory']
|
||||
@ldap_enabled = ENV['LDAP_ENABLED'] == 'true'
|
||||
@cas_enabled = ENV['CAS_ENABLED'] == 'true'
|
||||
@saml_enabled = ENV['SAML_ENABLED'] == 'true'
|
||||
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
|
||||
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
|
||||
@trending_hashtags = TrendingTags.get(10, filtered: false)
|
||||
@pending_reports_count = Report.unresolved.count
|
||||
@pending_tags_count = Tag.pending_review.count
|
||||
@authorized_fetch = authorized_fetch_mode?
|
||||
@whitelist_enabled = whitelist_mode?
|
||||
@profile_directory = Setting.profile_directory
|
||||
@timeline_preview = Setting.timeline_preview
|
||||
@keybase_integration = Setting.enable_keybase
|
||||
@trends_enabled = Setting.trends
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_week
|
||||
@current_week ||= Time.now.utc.to_date.cweek
|
||||
end
|
||||
|
||||
def redis_info
|
||||
@redis_info ||= begin
|
||||
if Redis.current.is_a?(Redis::Namespace)
|
||||
|
|
|
@ -14,7 +14,7 @@ module Admin
|
|||
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
|
||||
|
||||
if params[:media]
|
||||
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id))
|
||||
@statuses = @statuses.merge(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)).reorder('statuses.id desc')
|
||||
end
|
||||
|
||||
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
||||
|
|
|
@ -2,38 +2,12 @@
|
|||
|
||||
module Admin
|
||||
class TagsController < BaseController
|
||||
before_action :set_tag, except: [:index, :batch, :approve_all, :reject_all]
|
||||
before_action :set_usage_by_domain, except: [:index, :batch, :approve_all, :reject_all]
|
||||
before_action :set_counters, except: [:index, :batch, :approve_all, :reject_all]
|
||||
|
||||
def index
|
||||
authorize :tag, :index?
|
||||
|
||||
@tags = filtered_tags.page(params[:page])
|
||||
@form = Form::TagBatch.new
|
||||
end
|
||||
|
||||
def batch
|
||||
@form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
ensure
|
||||
redirect_to admin_tags_path(filter_params)
|
||||
end
|
||||
|
||||
def approve_all
|
||||
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'approve').save
|
||||
redirect_to admin_tags_path(filter_params)
|
||||
end
|
||||
|
||||
def reject_all
|
||||
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'reject').save
|
||||
redirect_to admin_tags_path(filter_params)
|
||||
end
|
||||
before_action :set_tag
|
||||
|
||||
def show
|
||||
authorize @tag, :show?
|
||||
|
||||
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
||||
end
|
||||
|
||||
def update
|
||||
|
@ -52,52 +26,8 @@ module Admin
|
|||
@tag = Tag.find(params[:id])
|
||||
end
|
||||
|
||||
def set_usage_by_domain
|
||||
@usage_by_domain = @tag.statuses
|
||||
.with_public_visibility
|
||||
.excluding_silenced_accounts
|
||||
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
|
||||
.joins(:account)
|
||||
.group('accounts.domain')
|
||||
.reorder(statuses_count: :desc)
|
||||
.pluck(Arel.sql('accounts.domain, count(*) AS statuses_count'))
|
||||
end
|
||||
|
||||
def set_counters
|
||||
@accounts_today = @tag.history.first[:accounts]
|
||||
@accounts_week = Redis.current.pfcount(*current_week_days.map { |day| "activity:tags:#{@tag.id}:#{day}:accounts" })
|
||||
end
|
||||
|
||||
def filtered_tags
|
||||
TagFilter.new(filter_params).results
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
||||
end
|
||||
|
||||
def tag_params
|
||||
params.require(:tag).permit(:name, :trendable, :usable, :listable)
|
||||
end
|
||||
|
||||
def current_week_days
|
||||
now = Time.now.utc.beginning_of_day.to_date
|
||||
|
||||
(Date.commercial(now.cwyear, now.cweek)..now).map do |date|
|
||||
date.to_time(:utc).beginning_of_day.to_i
|
||||
end
|
||||
end
|
||||
|
||||
def form_tag_batch_params
|
||||
params.require(:form_tag_batch).permit(:action, tag_ids: [])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:approve]
|
||||
'approve'
|
||||
elsif params[:reject]
|
||||
'reject'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseController
|
||||
def index
|
||||
authorize :preview_card_provider, :index?
|
||||
|
||||
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
|
||||
@form = Form::PreviewCardProviderBatch.new
|
||||
end
|
||||
|
||||
def batch
|
||||
@form = Form::PreviewCardProviderBatch.new(form_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
ensure
|
||||
redirect_to admin_trends_links_preview_card_providers_path(filter_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filtered_preview_card_providers
|
||||
PreviewCardProviderFilter.new(filter_params).results
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:page, *PreviewCardProviderFilter::KEYS).permit(:page, *PreviewCardProviderFilter::KEYS)
|
||||
end
|
||||
|
||||
def form_preview_card_provider_batch_params
|
||||
params.require(:form_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:approve]
|
||||
'approve'
|
||||
elsif params[:reject]
|
||||
'reject'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::Trends::LinksController < Admin::BaseController
|
||||
def index
|
||||
authorize :preview_card, :index?
|
||||
|
||||
@preview_cards = filtered_preview_cards.page(params[:page])
|
||||
@form = Form::PreviewCardBatch.new
|
||||
end
|
||||
|
||||
def batch
|
||||
@form = Form::PreviewCardBatch.new(form_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
ensure
|
||||
redirect_to admin_trends_links_path(filter_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filtered_preview_cards
|
||||
PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:page, *PreviewCardFilter::KEYS).permit(:page, *PreviewCardFilter::KEYS)
|
||||
end
|
||||
|
||||
def form_preview_card_batch_params
|
||||
params.require(:form_preview_card_batch).permit(:action, preview_card_ids: [])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:approve]
|
||||
'approve'
|
||||
elsif params[:approve_all]
|
||||
'approve_all'
|
||||
elsif params[:reject]
|
||||
'reject'
|
||||
elsif params[:reject_all]
|
||||
'reject_all'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::Trends::TagsController < Admin::BaseController
|
||||
def index
|
||||
authorize :tag, :index?
|
||||
|
||||
@tags = filtered_tags.page(params[:page])
|
||||
@form = Form::TagBatch.new
|
||||
end
|
||||
|
||||
def batch
|
||||
@form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
ensure
|
||||
redirect_to admin_trends_tags_path(filter_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filtered_tags
|
||||
TagFilter.new(filter_params).results
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
||||
end
|
||||
|
||||
def form_tag_batch_params
|
||||
params.require(:form_tag_batch).permit(:action, tag_ids: [])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:approve]
|
||||
'approve'
|
||||
elsif params[:reject]
|
||||
'reject'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::ProofsController < Api::BaseController
|
||||
include AccountOwnedConcern
|
||||
|
||||
skip_before_action :require_authenticated_user!
|
||||
|
||||
before_action :set_provider
|
||||
|
||||
def index
|
||||
render json: @account, serializer: @provider.serializer_class
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_provider
|
||||
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
def username_param
|
||||
params[:username]
|
||||
end
|
||||
end
|
|
@ -5,8 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
|||
before_action :set_account
|
||||
|
||||
def index
|
||||
@proofs = @account.suspended? ? [] : @account.identity_proofs.active
|
||||
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
||||
render json: []
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::AccountsController < Api::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
||||
|
@ -53,6 +53,11 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
def remove_from_followers
|
||||
RemoveFromFollowersService.new.call(current_user.account, @account)
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
def unblock
|
||||
UnblockService.new.call(current_user.account, @account)
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::DimensionsController < Api::BaseController
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
before_action :require_staff!
|
||||
before_action :set_dimensions
|
||||
|
||||
def create
|
||||
render json: @dimensions, each_serializer: REST::Admin::DimensionSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_dimensions
|
||||
@dimensions = Admin::Metrics::Dimension.retrieve(
|
||||
params[:keys],
|
||||
params[:start_at],
|
||||
params[:end_at],
|
||||
params[:limit],
|
||||
params
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::MeasuresController < Api::BaseController
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
before_action :require_staff!
|
||||
before_action :set_measures
|
||||
|
||||
def create
|
||||
render json: @measures, each_serializer: REST::Admin::MeasureSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_measures
|
||||
@measures = Admin::Metrics::Measure.retrieve(
|
||||
params[:keys],
|
||||
params[:start_at],
|
||||
params[:end_at],
|
||||
params
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::RetentionController < Api::BaseController
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
before_action :require_staff!
|
||||
before_action :set_cohorts
|
||||
|
||||
def create
|
||||
render json: @cohorts, each_serializer: REST::Admin::CohortSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_cohorts
|
||||
@cohorts = Admin::Metrics::Retention.new(
|
||||
params[:start_at],
|
||||
params[:end_at],
|
||||
params[:frequency]
|
||||
).cohorts
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::Trends::TagsController < Api::BaseController
|
||||
before_action :require_staff!
|
||||
before_action :set_tags
|
||||
|
||||
def index
|
||||
render json: @tags, each_serializer: REST::Admin::TagSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tags
|
||||
@tags = Trends.tags.get(false, limit_param(10))
|
||||
end
|
||||
end
|
|
@ -14,22 +14,21 @@ class Api::V1::Instances::ActivityController < Api::BaseController
|
|||
private
|
||||
|
||||
def activity
|
||||
weeks = []
|
||||
statuses_tracker = ActivityTracker.new('activity:statuses:local', :basic)
|
||||
logins_tracker = ActivityTracker.new('activity:logins', :unique)
|
||||
registrations_tracker = ActivityTracker.new('activity:accounts:local', :basic)
|
||||
|
||||
12.times do |i|
|
||||
day = i.weeks.ago.to_date
|
||||
week_id = day.cweek
|
||||
week = Date.commercial(day.cwyear, week_id)
|
||||
(0...12).map do |i|
|
||||
start_of_week = i.weeks.ago
|
||||
end_of_week = start_of_week + 6.days
|
||||
|
||||
weeks << {
|
||||
week: week.to_time.to_i.to_s,
|
||||
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0',
|
||||
logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s,
|
||||
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
|
||||
{
|
||||
week: start_of_week.to_i.to_s,
|
||||
statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s,
|
||||
logins: logins_tracker.sum(start_of_week, end_of_week).to_s,
|
||||
registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s,
|
||||
}
|
||||
end
|
||||
|
||||
weeks
|
||||
end
|
||||
|
||||
def require_enabled_api!
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Trends::LinksController < Api::BaseController
|
||||
before_action :set_links
|
||||
|
||||
def index
|
||||
render json: @links, each_serializer: REST::Trends::LinkSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_links
|
||||
@links = begin
|
||||
if Setting.trends
|
||||
Trends.links.get(true, limit_param(10))
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Trends::TagsController < Api::BaseController
|
||||
before_action :set_tags
|
||||
|
||||
def index
|
||||
render json: @tags, each_serializer: REST::TagSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tags
|
||||
@tags = begin
|
||||
if Setting.trends
|
||||
Trends.tags.get(true, limit_param(10))
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::TrendsController < Api::BaseController
|
||||
before_action :set_tags
|
||||
|
||||
def index
|
||||
render json: @tags, each_serializer: REST::TagSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tags
|
||||
@tags = TrendingTags.get(limit_param(10))
|
||||
end
|
||||
end
|
|
@ -11,7 +11,6 @@ class Auth::PasswordsController < Devise::PasswordsController
|
|||
super do |resource|
|
||||
if resource.errors.empty?
|
||||
resource.session_activations.destroy_all
|
||||
resource.forget_me!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
include Devise::Controllers::Rememberable
|
||||
include RegistrationSpamConcern
|
||||
|
||||
layout :determine_layout
|
||||
|
@ -31,8 +30,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
super do |resource|
|
||||
if resource.saved_change_to_encrypted_password?
|
||||
resource.clear_other_sessions(current_session.session_id)
|
||||
resource.forget_me!
|
||||
remember_me(resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Auth::SessionsController < Devise::SessionsController
|
||||
include Devise::Controllers::Rememberable
|
||||
|
||||
layout 'auth'
|
||||
|
||||
skip_before_action :require_no_authentication, only: [:create]
|
||||
|
@ -44,7 +42,7 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
end
|
||||
|
||||
def webauthn_options
|
||||
user = find_user
|
||||
user = User.find_by(id: session[:attempt_user_id])
|
||||
|
||||
if user&.webauthn_enabled?
|
||||
options_for_get = WebAuthn::Credential.options_for_get(
|
||||
|
@ -156,7 +154,6 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
clear_attempt_from_session
|
||||
|
||||
user.update_sign_in!(request, new_sign_in: true)
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
flash.delete(:notice)
|
||||
|
||||
|
|
|
@ -16,30 +16,7 @@ class HomeController < ApplicationController
|
|||
def redirect_unauthenticated_to_permalinks!
|
||||
return if user_signed_in?
|
||||
|
||||
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
|
||||
|
||||
if matches
|
||||
case matches[1]
|
||||
when 'statuses'
|
||||
status = Status.find_by(id: matches[2])
|
||||
|
||||
if status&.distributable?
|
||||
redirect_to(ActivityPub::TagManager.instance.url_for(status))
|
||||
return
|
||||
end
|
||||
when 'accounts'
|
||||
account = Account.find_by(id: matches[2])
|
||||
|
||||
if account
|
||||
redirect_to(ActivityPub::TagManager.instance.url_for(account))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
|
||||
|
||||
redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
|
||||
redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
|
||||
end
|
||||
|
||||
def set_pack
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::IdentityProofsController < Settings::BaseController
|
||||
before_action :check_required_params, only: :new
|
||||
before_action :check_enabled, only: :new
|
||||
|
||||
def index
|
||||
@proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc)
|
||||
@proofs.each(&:refresh!)
|
||||
end
|
||||
|
||||
def new
|
||||
@proof = current_account.identity_proofs.new(
|
||||
token: params[:token],
|
||||
provider: params[:provider],
|
||||
provider_username: params[:provider_username]
|
||||
)
|
||||
|
||||
if current_account.username.casecmp(params[:username]).zero?
|
||||
render layout: 'auth'
|
||||
else
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params)
|
||||
@proof.token = resource_params[:token]
|
||||
|
||||
if @proof.save
|
||||
PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof?
|
||||
redirect_to @proof.on_success_path(params[:user_agent])
|
||||
else
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@proof = current_account.identity_proofs.find(params[:id])
|
||||
@proof.destroy!
|
||||
redirect_to settings_identity_proofs_path, success: I18n.t('identity_proofs.removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_enabled
|
||||
not_found unless Setting.enable_keybase
|
||||
end
|
||||
|
||||
def check_required_params
|
||||
redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? }
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:account_identity_proof).permit(:provider, :provider_username, :token)
|
||||
end
|
||||
|
||||
def publish_proof?
|
||||
ActiveModel::Type::Boolean.new.cast(post_params[:post_status])
|
||||
end
|
||||
|
||||
def post_params
|
||||
params.require(:account_identity_proof).permit(:post_status, :status_text)
|
||||
end
|
||||
end
|
|
@ -1,17 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WellKnown
|
||||
class KeybaseProofConfigController < ActionController::Base
|
||||
before_action :check_enabled
|
||||
|
||||
def show
|
||||
render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer, root: 'keybase_config'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_enabled
|
||||
head 404 unless Setting.enable_keybase
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,8 @@ module Admin::FilterHelper
|
|||
CustomEmojiFilter::KEYS,
|
||||
ReportFilter::KEYS,
|
||||
TagFilter::KEYS,
|
||||
PreviewCardProviderFilter::KEYS,
|
||||
PreviewCardFilter::KEYS,
|
||||
InstanceFilter::KEYS,
|
||||
InviteFilter::KEYS,
|
||||
RelationshipFilter::KEYS,
|
||||
|
|
|
@ -137,6 +137,10 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
def react_admin_component(name, props = {})
|
||||
content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) })
|
||||
end
|
||||
|
||||
def body_classes
|
||||
output = (@body_classes || '').split(' ')
|
||||
output << "flavour-#{current_flavour.parameterize}"
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module LanguagesHelper
|
||||
HUMAN_LOCALES = {
|
||||
af: 'Afrikaans',
|
||||
ar: 'العربية',
|
||||
ast: 'Asturianu',
|
||||
bg: 'Български',
|
||||
bn: 'বাংলা',
|
||||
br: 'Breton',
|
||||
ca: 'Català',
|
||||
co: 'Corsu',
|
||||
cs: 'Čeština',
|
||||
cy: 'Cymraeg',
|
||||
da: 'Dansk',
|
||||
de: 'Deutsch',
|
||||
el: 'Ελληνικά',
|
||||
en: 'English',
|
||||
eo: 'Esperanto',
|
||||
'es-AR': 'Español (Argentina)',
|
||||
'es-MX': 'Español (México)',
|
||||
es: 'Español',
|
||||
et: 'Eesti',
|
||||
eu: 'Euskara',
|
||||
fa: 'فارسی',
|
||||
fi: 'Suomi',
|
||||
fr: 'Français',
|
||||
ga: 'Gaeilge',
|
||||
gd: 'Gàidhlig',
|
||||
gl: 'Galego',
|
||||
he: 'עברית',
|
||||
hi: 'हिन्दी',
|
||||
hr: 'Hrvatski',
|
||||
hu: 'Magyar',
|
||||
hy: 'Հայերեն',
|
||||
id: 'Bahasa Indonesia',
|
||||
io: 'Ido',
|
||||
is: 'Íslenska',
|
||||
it: 'Italiano',
|
||||
ja: '日本語',
|
||||
ka: 'ქართული',
|
||||
kab: 'Taqbaylit',
|
||||
kk: 'Қазақша',
|
||||
kmr: 'Kurmancî',
|
||||
kn: 'ಕನ್ನಡ',
|
||||
ko: '한국어',
|
||||
ku: 'سۆرانی',
|
||||
lt: 'Lietuvių',
|
||||
lv: 'Latviešu',
|
||||
mk: 'Македонски',
|
||||
ml: 'മലയാളം',
|
||||
mr: 'मराठी',
|
||||
ms: 'Bahasa Melayu',
|
||||
nl: 'Nederlands',
|
||||
nn: 'Nynorsk',
|
||||
no: 'Norsk',
|
||||
oc: 'Occitan',
|
||||
pl: 'Polski',
|
||||
'pt-BR': 'Português (Brasil)',
|
||||
'pt-PT': 'Português (Portugal)',
|
||||
pt: 'Português',
|
||||
ro: 'Română',
|
||||
ru: 'Русский',
|
||||
sa: 'संस्कृतम्',
|
||||
sc: 'Sardu',
|
||||
si: 'සිංහල',
|
||||
sk: 'Slovenčina',
|
||||
sl: 'Slovenščina',
|
||||
sq: 'Shqip',
|
||||
'sr-Latn': 'Srpski (latinica)',
|
||||
sr: 'Српски',
|
||||
sv: 'Svenska',
|
||||
ta: 'தமிழ்',
|
||||
te: 'తెలుగు',
|
||||
th: 'ไทย',
|
||||
tr: 'Türkçe',
|
||||
uk: 'Українська',
|
||||
ur: 'اُردُو',
|
||||
vi: 'Tiếng Việt',
|
||||
zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
|
||||
'zh-CN': '简体中文',
|
||||
'zh-HK': '繁體中文(香港)',
|
||||
'zh-TW': '繁體中文(臺灣)',
|
||||
zh: '中文',
|
||||
}.freeze
|
||||
|
||||
def human_locale(locale)
|
||||
if locale == 'und'
|
||||
I18n.t('generic.none')
|
||||
else
|
||||
HUMAN_LOCALES[locale.to_sym] || locale
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,94 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SettingsHelper
|
||||
HUMAN_LOCALES = {
|
||||
af: 'Afrikaans',
|
||||
ar: 'العربية',
|
||||
ast: 'Asturianu',
|
||||
bg: 'Български',
|
||||
bn: 'বাংলা',
|
||||
br: 'Breton',
|
||||
ca: 'Català',
|
||||
co: 'Corsu',
|
||||
cs: 'Čeština',
|
||||
cy: 'Cymraeg',
|
||||
da: 'Dansk',
|
||||
de: 'Deutsch',
|
||||
el: 'Ελληνικά',
|
||||
en: 'English',
|
||||
eo: 'Esperanto',
|
||||
'es-AR': 'Español (Argentina)',
|
||||
'es-MX': 'Español (México)',
|
||||
es: 'Español',
|
||||
et: 'Eesti',
|
||||
eu: 'Euskara',
|
||||
fa: 'فارسی',
|
||||
fi: 'Suomi',
|
||||
fr: 'Français',
|
||||
ga: 'Gaeilge',
|
||||
gd: 'Gàidhlig',
|
||||
gl: 'Galego',
|
||||
he: 'עברית',
|
||||
hi: 'हिन्दी',
|
||||
hr: 'Hrvatski',
|
||||
hu: 'Magyar',
|
||||
hy: 'Հայերեն',
|
||||
id: 'Bahasa Indonesia',
|
||||
io: 'Ido',
|
||||
is: 'Íslenska',
|
||||
it: 'Italiano',
|
||||
ja: '日本語',
|
||||
ka: 'ქართული',
|
||||
kab: 'Taqbaylit',
|
||||
kk: 'Қазақша',
|
||||
kn: 'ಕನ್ನಡ',
|
||||
ko: '한국어',
|
||||
ku: 'سۆرانی',
|
||||
lt: 'Lietuvių',
|
||||
lv: 'Latviešu',
|
||||
mk: 'Македонски',
|
||||
ml: 'മലയാളം',
|
||||
mr: 'मराठी',
|
||||
ms: 'Bahasa Melayu',
|
||||
nl: 'Nederlands',
|
||||
nn: 'Nynorsk',
|
||||
no: 'Norsk',
|
||||
oc: 'Occitan',
|
||||
pl: 'Polski',
|
||||
'pt-BR': 'Português (Brasil)',
|
||||
'pt-PT': 'Português (Portugal)',
|
||||
pt: 'Português',
|
||||
ro: 'Română',
|
||||
ru: 'Русский',
|
||||
sa: 'संस्कृतम्',
|
||||
sc: 'Sardu',
|
||||
si: 'සිංහල',
|
||||
sk: 'Slovenčina',
|
||||
sl: 'Slovenščina',
|
||||
sq: 'Shqip',
|
||||
'sr-Latn': 'Srpski (latinica)',
|
||||
sr: 'Српски',
|
||||
sv: 'Svenska',
|
||||
ta: 'தமிழ்',
|
||||
te: 'తెలుగు',
|
||||
th: 'ไทย',
|
||||
tr: 'Türkçe',
|
||||
uk: 'Українська',
|
||||
ur: 'اُردُو',
|
||||
vi: 'Tiếng Việt',
|
||||
zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
|
||||
'zh-CN': '简体中文',
|
||||
'zh-HK': '繁體中文(香港)',
|
||||
'zh-TW': '繁體中文(臺灣)',
|
||||
zh: '中文',
|
||||
}.freeze
|
||||
|
||||
def human_locale(locale)
|
||||
HUMAN_LOCALES[locale]
|
||||
end
|
||||
|
||||
def filterable_languages
|
||||
LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
|
||||
LanguageDetector.instance.language_names.select(&LanguagesHelper::HUMAN_LOCALES.method(:key?))
|
||||
end
|
||||
|
||||
def hash_to_object(hash)
|
||||
|
|
|
@ -5,6 +5,10 @@ export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
|||
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
|
||||
export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
|
||||
|
||||
export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
|
||||
export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
|
||||
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
|
||||
|
||||
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
|
||||
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
|
||||
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
|
||||
|
@ -104,6 +108,34 @@ export function fetchAccount(id) {
|
|||
};
|
||||
};
|
||||
|
||||
export const lookupAccount = acct => (dispatch, getState) => {
|
||||
dispatch(lookupAccountRequest(acct));
|
||||
|
||||
api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
|
||||
dispatch(fetchRelationships([response.data.id]));
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
dispatch(lookupAccountSuccess());
|
||||
}).catch(error => {
|
||||
dispatch(lookupAccountFail(acct, error));
|
||||
});
|
||||
};
|
||||
|
||||
export const lookupAccountRequest = (acct) => ({
|
||||
type: ACCOUNT_LOOKUP_REQUEST,
|
||||
acct,
|
||||
});
|
||||
|
||||
export const lookupAccountSuccess = () => ({
|
||||
type: ACCOUNT_LOOKUP_SUCCESS,
|
||||
});
|
||||
|
||||
export const lookupAccountFail = (acct, error) => ({
|
||||
type: ACCOUNT_LOOKUP_FAIL,
|
||||
acct,
|
||||
error,
|
||||
skipAlert: true,
|
||||
});
|
||||
|
||||
export function fetchAccountRequest(id) {
|
||||
return {
|
||||
type: ACCOUNT_FETCH_REQUEST,
|
||||
|
|
|
@ -83,7 +83,7 @@ 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');
|
||||
routerHistory.push('/publish');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -176,7 +176,8 @@ export function submitCompose(routerHistory) {
|
|||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
}).then(function (response) {
|
||||
if (routerHistory && routerHistory.location.pathname === '/statuses/new'
|
||||
if (routerHistory
|
||||
&& (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new')
|
||||
&& window.history.state
|
||||
&& !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) {
|
||||
routerHistory.goBack();
|
||||
|
|
|
@ -128,7 +128,7 @@ class Account extends ImmutablePureComponent {
|
|||
<Permalink
|
||||
className='account small'
|
||||
href={account.get('url')}
|
||||
to={`/accounts/${account.get('id')}`}
|
||||
to={`/@${account.get('acct')}`}
|
||||
>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar
|
||||
|
@ -144,7 +144,7 @@ class Account extends ImmutablePureComponent {
|
|||
) : (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
{mute_expires_at}
|
||||
<DisplayName account={account} />
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from 'flavours/glitch/util/api';
|
||||
import { FormattedNumber } from 'react-intl';
|
||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
import classNames from 'classnames';
|
||||
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||
|
||||
const percIncrease = (a, b) => {
|
||||
let percent;
|
||||
|
||||
if (b !== 0) {
|
||||
if (a !== 0) {
|
||||
percent = (b - a) / a;
|
||||
} else {
|
||||
percent = 1;
|
||||
}
|
||||
} else if (b === 0 && a === 0) {
|
||||
percent = 0;
|
||||
} else {
|
||||
percent = - 1;
|
||||
}
|
||||
|
||||
return percent;
|
||||
};
|
||||
|
||||
export default class Counter extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
measure: PropTypes.string.isRequired,
|
||||
start_at: PropTypes.string.isRequired,
|
||||
end_at: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
href: PropTypes.string,
|
||||
params: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
data: null,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { measure, start_at, end_at, params } = this.props;
|
||||
|
||||
api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const { label, href } = this.props;
|
||||
const { loading, data } = this.state;
|
||||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
content = (
|
||||
<React.Fragment>
|
||||
<span className='sparkline__value__total'><Skeleton width={43} /></span>
|
||||
<span className='sparkline__value__change'><Skeleton width={43} /></span>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
const measure = data[0];
|
||||
const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
|
||||
|
||||
content = (
|
||||
<React.Fragment>
|
||||
<span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span>
|
||||
<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const inner = (
|
||||
<React.Fragment>
|
||||
<div className='sparkline__value'>
|
||||
{content}
|
||||
</div>
|
||||
|
||||
<div className='sparkline__label'>
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<div className='sparkline__graph'>
|
||||
{!loading && (
|
||||
<Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}>
|
||||
<SparklinesCurve />
|
||||
</Sparklines>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a href={href} className='sparkline'>
|
||||
{inner}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='sparkline'>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from 'flavours/glitch/util/api';
|
||||
import { FormattedNumber } from 'react-intl';
|
||||
import { roundTo10 } from 'flavours/glitch/util/numbers';
|
||||
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||
|
||||
export default class Dimension extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dimension: PropTypes.string.isRequired,
|
||||
start_at: PropTypes.string.isRequired,
|
||||
end_at: PropTypes.string.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
params: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
data: null,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { start_at, end_at, dimension, limit, params } = this.props;
|
||||
|
||||
api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const { label, limit } = this.props;
|
||||
const { loading, data } = this.state;
|
||||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
content = (
|
||||
<table>
|
||||
<tbody>
|
||||
{Array.from(Array(limit)).map((_, i) => (
|
||||
<tr className='dimension__item' key={i}>
|
||||
<td className='dimension__item__key'>
|
||||
<Skeleton width={100} />
|
||||
</td>
|
||||
|
||||
<td className='dimension__item__value'>
|
||||
<Skeleton width={60} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
} else {
|
||||
const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
|
||||
|
||||
content = (
|
||||
<table>
|
||||
<tbody>
|
||||
{data[0].data.map(item => (
|
||||
<tr className='dimension__item' key={item.key}>
|
||||
<td className='dimension__item__key'>
|
||||
<span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} />
|
||||
<span title={item.key}>{item.human_key}</span>
|
||||
</td>
|
||||
|
||||
<td className='dimension__item__value'>
|
||||
{typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='dimension'>
|
||||
<h4>{label}</h4>
|
||||
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from 'flavours/glitch/util/api';
|
||||
import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { roundTo10 } from 'flavours/glitch/util/numbers';
|
||||
|
||||
const dateForCohort = cohort => {
|
||||
switch(cohort.frequency) {
|
||||
case 'day':
|
||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
|
||||
default:
|
||||
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
|
||||
}
|
||||
};
|
||||
|
||||
export default class Retention extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
start_at: PropTypes.string,
|
||||
end_at: PropTypes.string,
|
||||
frequency: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
data: null,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { start_at, end_at, frequency } = this.props;
|
||||
|
||||
api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const { loading, data } = this.state;
|
||||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
|
||||
} else {
|
||||
content = (
|
||||
<table className='retention__table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div className='retention__table__date retention__table__label'>
|
||||
<FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<div className='retention__table__number retention__table__label'>
|
||||
<FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
|
||||
</div>
|
||||
</th>
|
||||
|
||||
{data[0].data.slice(1).map((retention, i) => (
|
||||
<th key={retention.date}>
|
||||
<div className='retention__table__number retention__table__label'>
|
||||
{i + 1}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div className='retention__table__date retention__table__average'>
|
||||
<FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div className='retention__table__size'>
|
||||
<FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{data[0].data.slice(1).map((retention, i) => {
|
||||
const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].percent - sum)/(k + 1) : sum, 0);
|
||||
|
||||
return (
|
||||
<td key={retention.date}>
|
||||
<div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
|
||||
<FormattedNumber value={average} style='percent' />
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{data.slice(0, -1).map(cohort => (
|
||||
<tr key={cohort.period}>
|
||||
<td>
|
||||
<div className='retention__table__date'>
|
||||
{dateForCohort(cohort)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div className='retention__table__size'>
|
||||
<FormattedNumber value={cohort.data[0].value} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{cohort.data.slice(1).map(retention => (
|
||||
<td key={retention.date}>
|
||||
<div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.percent * 100)}`)}>
|
||||
<FormattedNumber value={retention.percent} style='percent' />
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='retention'>
|
||||
<h4><FormattedMessage id='admin.dashboard.retention' defaultMessage='Retention' /></h4>
|
||||
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from 'flavours/glitch/util/api';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||
|
||||
export default class Trends extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
limit: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
data: null,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { limit } = this.props;
|
||||
|
||||
api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const { limit } = this.props;
|
||||
const { loading, data } = this.state;
|
||||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
content = (
|
||||
<div>
|
||||
{Array.from(Array(limit)).map((_, i) => (
|
||||
<Hashtag key={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
{data.map(hashtag => (
|
||||
<Hashtag
|
||||
key={hashtag.name}
|
||||
name={hashtag.name}
|
||||
href={`/admin/tags/${hashtag.id}`}
|
||||
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
|
||||
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
|
||||
history={hashtag.history.reverse().map(day => day.uses)}
|
||||
className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='trends trends--compact'>
|
||||
<h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
|
||||
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -82,7 +82,7 @@ export default class AvatarComposite extends React.PureComponent {
|
|||
<a
|
||||
href={account.get('url')}
|
||||
target='_blank'
|
||||
onClick={(e) => this.props.onAccountClick(account.get('id'), e)}
|
||||
onClick={(e) => this.props.onAccountClick(account.get('acct'), e)}
|
||||
title={`@${account.get('acct')}`}
|
||||
key={account.get('id')}
|
||||
>
|
||||
|
|
|
@ -124,8 +124,8 @@ class ColumnHeader extends React.PureComponent {
|
|||
|
||||
moveButtons = (
|
||||
<div key='move-buttons' className='column-header__setting-arrows'>
|
||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
|
||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn && this.props.onPin) {
|
||||
|
@ -146,8 +146,8 @@ class ColumnHeader extends React.PureComponent {
|
|||
];
|
||||
|
||||
if (multiColumn) {
|
||||
collapsedContent.push(moveButtons);
|
||||
collapsedContent.push(pinButton);
|
||||
collapsedContent.push(moveButtons);
|
||||
}
|
||||
|
||||
if (children || (multiColumn && this.props.onPin)) {
|
||||
|
|
|
@ -61,7 +61,7 @@ export default class DisplayName extends React.PureComponent {
|
|||
<a
|
||||
href={a.get('url')}
|
||||
target='_blank'
|
||||
onClick={(e) => onAccountClick(a.get('id'), e)}
|
||||
onClick={(e) => onAccountClick(a.get('acct'), e)}
|
||||
title={`@${a.get('acct')}`}
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
|
@ -76,7 +76,7 @@ export default class DisplayName extends React.PureComponent {
|
|||
}
|
||||
|
||||
suffix = (
|
||||
<a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)} rel='noopener noreferrer'>
|
||||
<a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('acct'), e)} rel='noopener noreferrer'>
|
||||
<span className='display-name__account'>@{acct}</span>
|
||||
</a>
|
||||
);
|
||||
|
|
|
@ -6,6 +6,8 @@ import PropTypes from 'prop-types';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Permalink from './permalink';
|
||||
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||
import classNames from 'classnames';
|
||||
|
||||
class SilentErrorBoundary extends React.Component {
|
||||
|
||||
|
@ -47,45 +49,38 @@ const accountsCountRenderer = (displayNumber, pluralReady) => (
|
|||
/>
|
||||
);
|
||||
|
||||
const Hashtag = ({ hashtag }) => (
|
||||
<div className='trends__item'>
|
||||
export const ImmutableHashtag = ({ hashtag }) => (
|
||||
<Hashtag
|
||||
name={hashtag.get('name')}
|
||||
href={hashtag.get('url')}
|
||||
to={`/tags/${hashtag.get('name')}`}
|
||||
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
||||
uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1}
|
||||
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
||||
/>
|
||||
);
|
||||
|
||||
ImmutableHashtag.propTypes = {
|
||||
hashtag: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
const Hashtag = ({ name, href, to, people, uses, history, className }) => (
|
||||
<div className={classNames('trends__item', className)}>
|
||||
<div className='trends__item__name'>
|
||||
<Permalink
|
||||
href={hashtag.get('url')}
|
||||
to={`/timelines/tag/${hashtag.get('name')}`}
|
||||
>
|
||||
#<span>{hashtag.get('name')}</span>
|
||||
<Permalink href={href} to={to}>
|
||||
{name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
|
||||
</Permalink>
|
||||
|
||||
<ShortNumber
|
||||
value={
|
||||
hashtag.getIn(['history', 0, 'accounts']) * 1 +
|
||||
hashtag.getIn(['history', 1, 'accounts']) * 1
|
||||
}
|
||||
renderer={accountsCountRenderer}
|
||||
/>
|
||||
{typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}
|
||||
</div>
|
||||
|
||||
<div className='trends__item__current'>
|
||||
<ShortNumber
|
||||
value={
|
||||
hashtag.getIn(['history', 0, 'uses']) * 1 +
|
||||
hashtag.getIn(['history', 1, 'uses']) * 1
|
||||
}
|
||||
/>
|
||||
{typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />}
|
||||
</div>
|
||||
|
||||
<div className='trends__item__sparkline'>
|
||||
<SilentErrorBoundary>
|
||||
<Sparklines
|
||||
width={50}
|
||||
height={28}
|
||||
data={hashtag
|
||||
.get('history')
|
||||
.reverse()
|
||||
.map((day) => day.get('uses'))
|
||||
.toArray()}
|
||||
>
|
||||
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
</SilentErrorBoundary>
|
||||
|
@ -94,7 +89,13 @@ const Hashtag = ({ hashtag }) => (
|
|||
);
|
||||
|
||||
Hashtag.propTypes = {
|
||||
hashtag: ImmutablePropTypes.map.isRequired,
|
||||
name: PropTypes.string,
|
||||
href: PropTypes.string,
|
||||
to: PropTypes.string,
|
||||
people: PropTypes.number,
|
||||
uses: PropTypes.number,
|
||||
history: PropTypes.arrayOf(PropTypes.number),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Hashtag;
|
||||
|
|
|
@ -12,8 +12,18 @@ import RelativeTimestamp from './relative_timestamp';
|
|||
import Icon from 'flavours/glitch/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
|
||||
closed: {
|
||||
id: 'poll.closed',
|
||||
defaultMessage: 'Closed',
|
||||
},
|
||||
voted: {
|
||||
id: 'poll.voted',
|
||||
defaultMessage: 'You voted for this answer',
|
||||
},
|
||||
votes: {
|
||||
id: 'poll.votes',
|
||||
defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
|
||||
},
|
||||
});
|
||||
|
||||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||
|
@ -148,9 +158,16 @@ class Poll extends ImmutablePureComponent {
|
|||
data-index={optionIndex}
|
||||
/>
|
||||
)}
|
||||
{showResults && <span className='poll__number'>
|
||||
{Math.round(percent)}%
|
||||
</span>}
|
||||
{showResults && (
|
||||
<span
|
||||
className='poll__number'
|
||||
title={intl.formatMessage(messages.votes, {
|
||||
votes: option.get('votes_count'),
|
||||
})}
|
||||
>
|
||||
{Math.round(percent)}%
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span
|
||||
className='poll__option__text translate'
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>;
|
||||
|
||||
Skeleton.propTypes = {
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
};
|
||||
|
||||
export default Skeleton;
|
|
@ -346,7 +346,9 @@ class Status extends ImmutablePureComponent {
|
|||
return;
|
||||
} else {
|
||||
if (destination === undefined) {
|
||||
destination = `/statuses/${
|
||||
destination = `/@${
|
||||
status.getIn(['reblog', 'account', 'acct'], status.getIn(['account', 'acct']))
|
||||
}/${
|
||||
status.getIn(['reblog', 'id'], status.get('id'))
|
||||
}`;
|
||||
}
|
||||
|
@ -362,16 +364,6 @@ class Status extends ImmutablePureComponent {
|
|||
this.setState({ showMedia: !this.state.showMedia });
|
||||
}
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
if (this.context.router && e.button === 0) {
|
||||
const id = e.currentTarget.getAttribute('data-id');
|
||||
e.preventDefault();
|
||||
let state = {...this.context.router.history.location.state};
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
this.context.router.history.push(`/accounts/${id}`, state);
|
||||
}
|
||||
}
|
||||
|
||||
handleExpandedToggle = () => {
|
||||
if (this.props.status.get('spoiler_text')) {
|
||||
this.setExpansion(!this.state.isExpanded);
|
||||
|
@ -433,13 +425,14 @@ class Status extends ImmutablePureComponent {
|
|||
handleHotkeyOpen = () => {
|
||||
let state = {...this.context.router.history.location.state};
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`, state);
|
||||
const status = this.props.status;
|
||||
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state);
|
||||
}
|
||||
|
||||
handleHotkeyOpenProfile = () => {
|
||||
let state = {...this.context.router.history.location.state};
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`, state);
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
|
||||
}
|
||||
|
||||
handleHotkeyMoveUp = e => {
|
||||
|
|
|
@ -148,10 +148,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
handleOpen = () => {
|
||||
let state = {...this.context.router.history.location.state};
|
||||
if (state.mastodonModalKey) {
|
||||
this.context.router.history.replace(`/statuses/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 });
|
||||
this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 });
|
||||
} else {
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`, state);
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -197,7 +197,7 @@ export default class StatusContent extends React.PureComponent {
|
|||
|
||||
onMentionClick = (mention, e) => {
|
||||
if (this.props.parseClick) {
|
||||
this.props.parseClick(e, `/accounts/${mention.get('id')}`);
|
||||
this.props.parseClick(e, `/@${mention.get('acct')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,7 +205,7 @@ export default class StatusContent extends React.PureComponent {
|
|||
hashtag = hashtag.replace(/^#/, '');
|
||||
|
||||
if (this.props.parseClick) {
|
||||
this.props.parseClick(e, `/timelines/tag/${hashtag}`);
|
||||
this.props.parseClick(e, `/tags/${hashtag}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,7 +277,7 @@ export default class StatusContent extends React.PureComponent {
|
|||
|
||||
const mentionLinks = status.get('mentions').map(item => (
|
||||
<Permalink
|
||||
to={`/accounts/${item.get('id')}`}
|
||||
to={`/@${item.get('acct')}`}
|
||||
href={item.get('url')}
|
||||
key={item.get('id')}
|
||||
className='mention'
|
||||
|
|
|
@ -19,14 +19,14 @@ export default class StatusHeader extends React.PureComponent {
|
|||
};
|
||||
|
||||
// Handles clicks on account name/image
|
||||
handleClick = (id, e) => {
|
||||
handleClick = (acct, e) => {
|
||||
const { parseClick } = this.props;
|
||||
parseClick(e, `/accounts/${id}`);
|
||||
parseClick(e, `/@${acct}`);
|
||||
}
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
const { status } = this.props;
|
||||
this.handleClick(status.getIn(['account', 'id']), e);
|
||||
this.handleClick(status.getIn(['account', 'acct']), e);
|
||||
}
|
||||
|
||||
// Rendering.
|
||||
|
|
|
@ -17,7 +17,7 @@ export default class StatusPrepend extends React.PureComponent {
|
|||
|
||||
handleClick = (e) => {
|
||||
const { account, parseClick } = this.props;
|
||||
parseClick(e, `/accounts/${account.get('id')}`);
|
||||
parseClick(e, `/@${account.get('acct')}`);
|
||||
}
|
||||
|
||||
Message = () => {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { getLocale } from 'mastodon/locales';
|
||||
|
||||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
|
||||
export default class AdminComponent extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { locale, children } = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -23,14 +23,38 @@ store.dispatch(hydrateAction);
|
|||
// load custom emojis
|
||||
store.dispatch(fetchCustomEmojis());
|
||||
|
||||
const createIdentityContext = state => ({
|
||||
signedIn: !!state.meta.me,
|
||||
accountId: state.meta.me,
|
||||
accessToken: state.meta.access_token,
|
||||
});
|
||||
|
||||
export default class Mastodon extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
static childContextTypes = {
|
||||
identity: PropTypes.shape({
|
||||
signedIn: PropTypes.bool.isRequired,
|
||||
accountId: PropTypes.string,
|
||||
accessToken: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
identity = createIdentityContext(initialState);
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
identity: this.identity,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.disconnect = store.dispatch(connectUserStream());
|
||||
if (this.identity.signedIn) {
|
||||
this.disconnect = store.dispatch(connectUserStream());
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { getLocale } from 'mastodon/locales';
|
|||
import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar';
|
||||
import MediaGallery from 'flavours/glitch/components/media_gallery';
|
||||
import Poll from 'flavours/glitch/components/poll';
|
||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
|
||||
import ModalRoot from 'flavours/glitch/components/modal_root';
|
||||
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
|
||||
import Video from 'flavours/glitch/features/video';
|
||||
|
|
|
@ -62,17 +62,17 @@ class ActionBar extends React.PureComponent {
|
|||
|
||||
<div className='account__action-bar'>
|
||||
<div className='account__action-bar-links'>
|
||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
|
||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}`}>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||
<strong><FormattedNumber value={account.get('statuses_count')} /></strong>
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
|
||||
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}/following`}>
|
||||
<FormattedMessage id='account.follows' defaultMessage='Follows' />
|
||||
<strong><FormattedNumber value={account.get('following_count')} /></strong>
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
|
||||
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}/followers`}>
|
||||
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
||||
<strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
|
||||
</NavLink>
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
|
||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
|
@ -16,13 +16,24 @@ import LoadMore from 'flavours/glitch/components/load_more';
|
|||
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
attachments: getAccountGallery(state, props.params.accountId),
|
||||
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||
suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false),
|
||||
});
|
||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
||||
|
||||
if (!accountId) {
|
||||
return {
|
||||
isLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
attachments: getAccountGallery(state, accountId),
|
||||
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
};
|
||||
};
|
||||
|
||||
class LoadMoreMedia extends ImmutablePureComponent {
|
||||
|
||||
|
@ -50,7 +61,11 @@ export default @connect(mapStateToProps)
|
|||
class AccountGallery extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
params: PropTypes.shape({
|
||||
acct: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
accountId: PropTypes.string,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
attachments: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
|
@ -64,15 +79,30 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
width: 323,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
|
||||
_load () {
|
||||
const { accountId, isAccount, dispatch } = this.props;
|
||||
|
||||
if (!isAccount) dispatch(fetchAccount(accountId));
|
||||
dispatch(expandAccountMediaTimeline(accountId));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
|
||||
componentDidMount () {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (accountId) {
|
||||
this._load();
|
||||
} else {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (prevProps.accountId !== accountId && accountId) {
|
||||
this._load();
|
||||
} else if (prevProps.params.acct !== acct) {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +126,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId }));
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
|
||||
};
|
||||
|
||||
handleLoadOlder = e => {
|
||||
|
@ -162,7 +192,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
|
||||
<ScrollContainer scrollKey='account_gallery'>
|
||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
<HeaderContainer accountId={this.props.accountId} />
|
||||
|
||||
{suspended ? (
|
||||
<div className='empty-column-indicator'>
|
||||
|
|
|
@ -128,9 +128,9 @@ export default class Header extends ImmutablePureComponent {
|
|||
|
||||
{!hideTabs && (
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
|
||||
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
|
||||
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
|
||||
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
|
||||
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
|
||||
<NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,7 @@ export default class MovedNote extends ImmutablePureComponent {
|
|||
e.preventDefault();
|
||||
let state = {...this.context.router.history.location.state};
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
this.context.router.history.push(`/accounts/${this.props.to.get('id')}`, state);
|
||||
this.context.router.history.push(`/@${this.props.to.get('acct')}`, state);
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines';
|
||||
import StatusList from '../../components/status_list';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
|
@ -19,10 +19,19 @@ import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
|||
|
||||
const emptyList = ImmutableList();
|
||||
|
||||
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
|
||||
const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) => {
|
||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
||||
|
||||
if (!accountId) {
|
||||
return {
|
||||
isLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
||||
|
||||
return {
|
||||
accountId,
|
||||
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
|
||||
remoteUrl: state.getIn(['accounts', accountId, 'url']),
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
|
@ -46,7 +55,11 @@ export default @connect(mapStateToProps)
|
|||
class AccountTimeline extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
params: PropTypes.shape({
|
||||
acct: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
accountId: PropTypes.string,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list,
|
||||
featuredStatusIds: ImmutablePropTypes.list,
|
||||
|
@ -60,25 +73,47 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { accountId }, withReplies } = this.props;
|
||||
_load () {
|
||||
const { accountId, withReplies, dispatch } = this.props;
|
||||
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(accountId));
|
||||
dispatch(fetchAccount(accountId));
|
||||
dispatch(fetchAccountIdentityProofs(accountId));
|
||||
if (!withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
}
|
||||
dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (accountId) {
|
||||
this._load();
|
||||
} else {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (prevProps.accountId !== accountId && accountId) {
|
||||
this._load();
|
||||
} else if (prevProps.params.acct !== acct) {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
|
||||
dispatch(fetchAccount(nextProps.params.accountId));
|
||||
dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
|
||||
if (!nextProps.withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
|
||||
dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
|
||||
}
|
||||
this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
|
||||
dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +122,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
|
||||
this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies }));
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
|
@ -131,7 +166,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
|
||||
|
||||
<StatusList
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
||||
prepend={<HeaderContainer accountId={this.props.accountId} />}
|
||||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
scrollKey='account_timeline'
|
||||
|
|
|
@ -87,7 +87,7 @@ class Header extends ImmutablePureComponent {
|
|||
<Link
|
||||
aria-label={intl.formatMessage(messages.home_timeline)}
|
||||
title={intl.formatMessage(messages.home_timeline)}
|
||||
to='/timelines/home'
|
||||
to='/home'
|
||||
><Icon id='home' /></Link>
|
||||
))}
|
||||
{renderForColumn('NOTIFICATIONS', (
|
||||
|
@ -106,14 +106,14 @@ class Header extends ImmutablePureComponent {
|
|||
<Link
|
||||
aria-label={intl.formatMessage(messages.community)}
|
||||
title={intl.formatMessage(messages.community)}
|
||||
to='/timelines/public/local'
|
||||
to='/public/local'
|
||||
><Icon id='users' /></Link>
|
||||
))}
|
||||
{renderForColumn('PUBLIC', (
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.public)}
|
||||
title={intl.formatMessage(messages.public)}
|
||||
to='/timelines/public'
|
||||
to='/public'
|
||||
><Icon id='globe' /></Link>
|
||||
))}
|
||||
<a
|
||||
|
|
|
@ -15,13 +15,13 @@ export default class NavigationBar extends ImmutablePureComponent {
|
|||
render () {
|
||||
return (
|
||||
<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={`/@${this.props.account.get('acct')}`}>
|
||||
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
|
||||
<Avatar account={this.props.account} size={48} />
|
||||
</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={`/@${this.props.account.get('acct')}`}>
|
||||
<strong>@{this.props.account.get('acct')}</strong>
|
||||
</Permalink>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
|||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import { searchEnabled } from 'flavours/glitch/util/initial_state';
|
||||
import LoadMore from 'flavours/glitch/components/load_more';
|
||||
|
|
|
@ -104,7 +104,7 @@ class Conversation extends ImmutablePureComponent {
|
|||
markRead();
|
||||
}
|
||||
|
||||
this.context.router.history.push(`/statuses/${lastStatus.get('id')}`);
|
||||
this.context.router.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
|
||||
}
|
||||
|
||||
handleMarkAsRead = () => {
|
||||
|
@ -163,7 +163,7 @@ class Conversation extends ImmutablePureComponent {
|
|||
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
|
||||
|
||||
const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
|
||||
const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
|
||||
|
||||
const handlers = {
|
||||
reply: this.handleReply,
|
||||
|
|
|
@ -213,7 +213,7 @@ class AccountCard extends ImmutablePureComponent {
|
|||
<Permalink
|
||||
className='directory__card__bar__name'
|
||||
href={account.get('url')}
|
||||
to={`/accounts/${account.get('id')}`}
|
||||
to={`/@${account.get('acct')}`}
|
||||
>
|
||||
<Avatar account={account} size={48} />
|
||||
<DisplayName account={account} />
|
||||
|
|
|
@ -66,7 +66,7 @@ class Account extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='account follow-recommendations-account'>
|
||||
<div className='account__wrapper'>
|
||||
<Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||
<Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
|
||||
<DisplayName account={account} />
|
||||
|
|
|
@ -68,7 +68,7 @@ class FollowRecommendations extends ImmutablePureComponent {
|
|||
}
|
||||
}));
|
||||
|
||||
router.history.push('/timelines/home');
|
||||
router.history.push('/home');
|
||||
}
|
||||
|
||||
render () {
|
||||
|
|
|
@ -30,7 +30,7 @@ class AccountAuthorize extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='account-authorize__wrapper'>
|
||||
<div className='account-authorize'>
|
||||
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
|
||||
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
|
|
@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { debounce } from 'lodash';
|
||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import {
|
||||
lookupAccount,
|
||||
fetchAccount,
|
||||
fetchFollowers,
|
||||
expandFollowers,
|
||||
|
@ -19,14 +20,25 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
|||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])),
|
||||
remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'followers', props.params.accountId, 'isLoading'], true),
|
||||
});
|
||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
||||
|
||||
if (!accountId) {
|
||||
return {
|
||||
isLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
|
||||
remoteUrl: state.getIn(['accounts', accountId, 'url']),
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
|
||||
};
|
||||
};
|
||||
|
||||
const RemoteHint = ({ url }) => (
|
||||
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} />
|
||||
|
@ -40,7 +52,11 @@ export default @connect(mapStateToProps)
|
|||
class Followers extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
params: PropTypes.shape({
|
||||
acct: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
accountId: PropTypes.string,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
|
@ -51,32 +67,45 @@ class Followers extends ImmutablePureComponent {
|
|||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||
this.props.dispatch(fetchFollowers(this.props.params.accountId));
|
||||
_load () {
|
||||
const { accountId, isAccount, dispatch } = this.props;
|
||||
|
||||
if (!isAccount) dispatch(fetchAccount(accountId));
|
||||
dispatch(fetchFollowers(accountId));
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (accountId) {
|
||||
this._load();
|
||||
} else {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(fetchFollowers(nextProps.params.accountId));
|
||||
}
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
if (prevProps.accountId !== accountId && accountId) {
|
||||
this._load();
|
||||
} else if (prevProps.params.acct !== acct) {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowers(this.props.params.accountId));
|
||||
this.props.dispatch(expandFollowers(this.props.accountId));
|
||||
}, 300, { leading: true });
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
|
||||
|
||||
|
@ -115,7 +144,7 @@ class Followers extends ImmutablePureComponent {
|
|||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
|
||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
emptyMessage={emptyMessage}
|
||||
|
|
|
@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { debounce } from 'lodash';
|
||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import {
|
||||
lookupAccount,
|
||||
fetchAccount,
|
||||
fetchFollowing,
|
||||
expandFollowing,
|
||||
|
@ -19,14 +20,25 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
|||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])),
|
||||
remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'following', props.params.accountId, 'isLoading'], true),
|
||||
});
|
||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
||||
|
||||
if (!accountId) {
|
||||
return {
|
||||
isLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
|
||||
remoteUrl: state.getIn(['accounts', accountId, 'url']),
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
|
||||
};
|
||||
};
|
||||
|
||||
const RemoteHint = ({ url }) => (
|
||||
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} />
|
||||
|
@ -40,7 +52,11 @@ export default @connect(mapStateToProps)
|
|||
class Following extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
params: PropTypes.shape({
|
||||
acct: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
accountId: PropTypes.string,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
|
@ -51,32 +67,45 @@ class Following extends ImmutablePureComponent {
|
|||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||
this.props.dispatch(fetchFollowing(this.props.params.accountId));
|
||||
_load () {
|
||||
const { accountId, isAccount, dispatch } = this.props;
|
||||
|
||||
if (!isAccount) dispatch(fetchAccount(accountId));
|
||||
dispatch(fetchFollowing(accountId));
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (accountId) {
|
||||
this._load();
|
||||
} else {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(fetchFollowing(nextProps.params.accountId));
|
||||
}
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
if (prevProps.accountId !== accountId && accountId) {
|
||||
this._load();
|
||||
} else if (prevProps.params.acct !== acct) {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowing(this.props.params.accountId));
|
||||
this.props.dispatch(expandFollowing(this.props.accountId));
|
||||
}, 300, { leading: true });
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
|
||||
|
||||
|
@ -115,7 +144,7 @@ class Following extends ImmutablePureComponent {
|
|||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
|
||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
emptyMessage={emptyMessage}
|
||||
|
|
|
@ -87,7 +87,7 @@ class Content extends ImmutablePureComponent {
|
|||
onMentionClick = (mention, e) => {
|
||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/accounts/${mention.get('id')}`);
|
||||
this.context.router.history.push(`/@${mention.get('acct')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,14 +96,14 @@ class Content extends ImmutablePureComponent {
|
|||
|
||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/timelines/tag/${hashtag}`);
|
||||
this.context.router.history.push(`/tags/${hashtag}`);
|
||||
}
|
||||
}
|
||||
|
||||
onStatusClick = (status, e) => {
|
||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/statuses/${status.get('id')}`);
|
||||
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class Trends extends ImmutablePureComponent {
|
||||
|
|
|
@ -107,7 +107,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
|||
const { fetchFollowRequests, multiColumn } = this.props;
|
||||
|
||||
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
|
||||
this.context.router.history.replace('/timelines/home');
|
||||
this.context.router.history.replace('/home');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
|||
|
||||
if (multiColumn) {
|
||||
if (!columns.find(item => item.get('id') === 'HOME')) {
|
||||
navItems.push(<ColumnLink key='0' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />);
|
||||
navItems.push(<ColumnLink key='0' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />);
|
||||
}
|
||||
|
||||
if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) {
|
||||
|
@ -130,16 +130,16 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
|||
}
|
||||
|
||||
if (!columns.find(item => item.get('id') === 'COMMUNITY')) {
|
||||
navItems.push(<ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />);
|
||||
navItems.push(<ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />);
|
||||
}
|
||||
|
||||
if (!columns.find(item => item.get('id') === 'PUBLIC')) {
|
||||
navItems.push(<ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />);
|
||||
navItems.push(<ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />);
|
||||
}
|
||||
}
|
||||
|
||||
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
|
||||
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />);
|
||||
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />);
|
||||
}
|
||||
|
||||
if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) {
|
||||
|
@ -160,7 +160,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
|||
<div key='9'>
|
||||
<ColumnLink key='10' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
{lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list =>
|
||||
<ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
<ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
)}
|
||||
</div>,
|
||||
]);
|
||||
|
|
|
@ -11,21 +11,22 @@ const mapStateToProps = (state, { columnId }) => {
|
|||
return {};
|
||||
}
|
||||
|
||||
return { settings: columns.get(index).get('params') };
|
||||
return {
|
||||
settings: columns.get(index).get('params'),
|
||||
onLoad (value) {
|
||||
return api(() => state).get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
|
||||
return (response.data.hashtags || []).map((tag) => {
|
||||
return { value: tag.name, label: `#${tag.name}` };
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { columnId }) => ({
|
||||
onChange (key, value) {
|
||||
dispatch(changeColumnParams(columnId, key, value));
|
||||
},
|
||||
|
||||
onLoad (value) {
|
||||
return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
|
||||
return (response.data.hashtags || []).map((tag) => {
|
||||
return { value: tag.name, label: `#${tag.name}` };
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
||||
|
|
|
@ -73,7 +73,7 @@ class Lists extends ImmutablePureComponent {
|
|||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{lists.map(list =>
|
||||
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
|
||||
<ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
|
|
|
@ -27,11 +27,12 @@ export default class ColumnSettings extends React.PureComponent {
|
|||
render () {
|
||||
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
|
||||
|
||||
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
|
||||
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
|
||||
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
|
||||
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||
|
||||
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
||||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||
|
@ -58,11 +59,11 @@ export default class ColumnSettings extends React.PureComponent {
|
|||
|
||||
<div role='group' aria-labelledby='notifications-unread-markers'>
|
||||
<span id='notifications-unread-markers' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.unread_markers.category' defaultMessage='Unread notification markers' />
|
||||
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
|
||||
</span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={filterShowStr} />
|
||||
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -72,7 +73,7 @@ export default class ColumnSettings extends React.PureComponent {
|
|||
</span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
|
||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
|
||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -39,7 +39,7 @@ export default class NotificationFollow extends ImmutablePureComponent {
|
|||
|
||||
handleOpenProfile = () => {
|
||||
const { notification } = this.props;
|
||||
this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`);
|
||||
this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
|
||||
}
|
||||
|
||||
handleMention = e => {
|
||||
|
@ -70,7 +70,7 @@ export default class NotificationFollow extends ImmutablePureComponent {
|
|||
className='notification__display-name'
|
||||
href={account.get('url')}
|
||||
title={account.get('acct')}
|
||||
to={`/accounts/${account.get('id')}`}
|
||||
to={`/@${account.get('acct')}`}
|
||||
dangerouslySetInnerHTML={{ __html: displayName }}
|
||||
/></bdi>
|
||||
);
|
||||
|
|
|
@ -45,7 +45,7 @@ class FollowRequest extends ImmutablePureComponent {
|
|||
|
||||
handleOpenProfile = () => {
|
||||
const { notification } = this.props;
|
||||
this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`);
|
||||
this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
|
||||
}
|
||||
|
||||
handleMention = e => {
|
||||
|
@ -89,7 +89,7 @@ class FollowRequest extends ImmutablePureComponent {
|
|||
className='notification__display-name'
|
||||
href={account.get('url')}
|
||||
title={account.get('acct')}
|
||||
to={`/accounts/${account.get('id')}`}
|
||||
to={`/@${account.get('acct')}`}
|
||||
dangerouslySetInnerHTML={{ __html: displayName }}
|
||||
/></bdi>
|
||||
);
|
||||
|
@ -111,7 +111,7 @@ class FollowRequest extends ImmutablePureComponent {
|
|||
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
|
|
@ -122,7 +122,7 @@ class Footer extends ImmutablePureComponent {
|
|||
onClose();
|
||||
}
|
||||
|
||||
router.history.push(`/statuses/${status.get('id')}`);
|
||||
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||
}
|
||||
|
||||
render () {
|
||||
|
|
|
@ -34,7 +34,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='picture-in-picture__header'>
|
||||
<Link to={`/statuses/${statusId}`} className='picture-in-picture__header__account'>
|
||||
<Link to={`/@${account.get('acct')}/${statusId}`} className='picture-in-picture__header__account'>
|
||||
<Avatar account={account} size={36} />
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
|
|
|
@ -51,7 +51,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
e.preventDefault();
|
||||
let state = {...this.context.router.history.location.state};
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`, state);
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
|
@ -216,7 +216,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
reblogLink = (
|
||||
<React.Fragment>
|
||||
<React.Fragment> · </React.Fragment>
|
||||
<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<Icon id={reblogIcon} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||
|
@ -240,7 +240,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
|
||||
if (this.context.router) {
|
||||
favouriteLink = (
|
||||
<Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
|
||||
<Icon id='star' />
|
||||
<span className='detailed-status__favorites'>
|
||||
<AnimatedNumber value={status.get('favourites_count')} />
|
||||
|
|
|
@ -70,7 +70,7 @@ const makeMapStateToProps = () => {
|
|||
ancestorsIds = ancestorsIds.withMutations(mutable => {
|
||||
let id = statusId;
|
||||
|
||||
while (id) {
|
||||
while (id && !mutable.includes(id)) {
|
||||
mutable.unshift(id);
|
||||
id = inReplyTos.get(id);
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ const makeMapStateToProps = () => {
|
|||
const ids = [statusId];
|
||||
|
||||
while (ids.length > 0) {
|
||||
let id = ids.shift();
|
||||
let id = ids.pop();
|
||||
const replies = contextReplies.get(id);
|
||||
|
||||
if (statusId !== id) {
|
||||
|
@ -97,7 +97,7 @@ const makeMapStateToProps = () => {
|
|||
|
||||
if (replies) {
|
||||
replies.reverse().forEach(reply => {
|
||||
ids.unshift(reply);
|
||||
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -405,7 +405,7 @@ class Status extends ImmutablePureComponent {
|
|||
handleHotkeyOpenProfile = () => {
|
||||
let state = {...this.context.router.history.location.state};
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`, state);
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
|
||||
}
|
||||
|
||||
handleMoveUp = id => {
|
||||
|
|
|
@ -69,7 +69,7 @@ class BoostModal extends ImmutablePureComponent {
|
|||
this.props.onClose();
|
||||
let state = {...this.context.router.history.location.state};
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`, state);
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ const componentMap = {
|
|||
'DIRECTORY': Directory,
|
||||
};
|
||||
|
||||
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started|^\/start/);
|
||||
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/search|^\/getting-started|^\/start/);
|
||||
|
||||
const messages = defineMessages({
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
||||
|
@ -216,7 +216,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
||||
|
||||
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)}><Icon id='pencil' /></Link>;
|
||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||
|
||||
const content = columnIndex !== -1 ? (
|
||||
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={!swipeToChangeColumns}>
|
||||
|
|
|
@ -49,7 +49,7 @@ class FavouriteModal extends ImmutablePureComponent {
|
|||
this.props.onClose();
|
||||
let state = {...this.context.router.history.location.state};
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`, state);
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class ListPanel extends ImmutablePureComponent {
|
|||
<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' id='list-ul' fixedWidth />{list.get('title')}</NavLink>
|
||||
<NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/lists/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -112,15 +112,6 @@ class MediaModal extends ImmutablePureComponent {
|
|||
}));
|
||||
};
|
||||
|
||||
handleStatusClick = e => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/statuses/${this.props.statusId}`);
|
||||
}
|
||||
|
||||
this._sendBackgroundColor();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (prevState.index !== this.state.index) {
|
||||
this._sendBackgroundColor();
|
||||
|
|
|
@ -11,12 +11,12 @@ import TrendsContainer from 'flavours/glitch/features/getting_started/containers
|
|||
|
||||
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' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='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' id='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' id='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' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='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' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
|
||||
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
|
||||
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
||||
|
|
|
@ -79,7 +79,7 @@ const PageThree = ({ intl, myAccount }) => (
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }} /></p>
|
||||
<p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }} /></p>
|
||||
<p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p>
|
||||
</div>
|
||||
);
|
||||
|
@ -130,7 +130,7 @@ const PageSix = ({ admin, domain }) => {
|
|||
if (admin) {
|
||||
adminSection = (
|
||||
<p>
|
||||
<FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} />
|
||||
<FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/@${admin.get('acct')}`}>@{admin.get('acct')}</Permalink> }} />
|
||||
<br />
|
||||
<FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage="Please read {domain}'s {guidelines}!" values={{ domain, guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }} />
|
||||
</p>
|
||||
|
|
|
@ -8,10 +8,10 @@ import Icon from 'flavours/glitch/components/icon';
|
|||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
|
||||
export const links = [
|
||||
<NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></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' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><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' ><Icon id='bars' fixedWidth /></NavLink>,
|
||||
];
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue