Merge branch 'upstream' into main
This commit is contained in:
commit
b5f59575f6
|
|
@ -20,3 +20,9 @@ postgres14
|
||||||
redis
|
redis
|
||||||
elasticsearch
|
elasticsearch
|
||||||
chart
|
chart
|
||||||
|
.yarn/
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
|
||||||
|
|
@ -316,3 +316,24 @@ MAX_POLL_OPTION_CHARS=100
|
||||||
# -----------------------
|
# -----------------------
|
||||||
IP_RETENTION_PERIOD=31556952
|
IP_RETENTION_PERIOD=31556952
|
||||||
SESSION_RETENTION_PERIOD=31556952
|
SESSION_RETENTION_PERIOD=31556952
|
||||||
|
|
||||||
|
# Fetch All Replies Behavior
|
||||||
|
# --------------------------
|
||||||
|
# When a user expands a post (DetailedStatus view), fetch all of its replies
|
||||||
|
# (default: false)
|
||||||
|
FETCH_REPLIES_ENABLED=false
|
||||||
|
|
||||||
|
# Period to wait between fetching replies (in minutes)
|
||||||
|
FETCH_REPLIES_COOLDOWN_MINUTES=15
|
||||||
|
|
||||||
|
# Period to wait after a post is first created before fetching its replies (in minutes)
|
||||||
|
FETCH_REPLIES_INITIAL_WAIT_MINUTES=5
|
||||||
|
|
||||||
|
# Max number of replies to fetch - total, recursively through a whole reply tree
|
||||||
|
FETCH_REPLIES_MAX_GLOBAL=1000
|
||||||
|
|
||||||
|
# Max number of replies to fetch - for a single post
|
||||||
|
FETCH_REPLIES_MAX_SINGLE=500
|
||||||
|
|
||||||
|
# Max number of replies Collection pages to fetch - total
|
||||||
|
FETCH_REPLIES_MAX_PAGES=500
|
||||||
|
|
|
||||||
|
|
@ -43,4 +43,4 @@ jobs:
|
||||||
- name: Run haml-lint
|
- name: Run haml-lint
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
|
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
|
||||||
bin/haml-lint --parallel --reporter github
|
bin/haml-lint --reporter github
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ on:
|
||||||
- .github/workflows/test-image-build.yml
|
- .github/workflows/test-image-build.yml
|
||||||
- Dockerfile
|
- Dockerfile
|
||||||
- streaming/Dockerfile
|
- streaming/Dockerfile
|
||||||
|
- .dockerignore
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,18 @@ jobs:
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|
||||||
|
- name: Ensure no errors with `db:prepare`
|
||||||
|
run: |
|
||||||
|
bin/rails db:drop
|
||||||
|
bin/rails db:prepare
|
||||||
|
bin/rails db:migrate
|
||||||
|
|
||||||
|
- name: Ensure no errors with `db:prepare` and SKIP_POST_DEPLOYMENT_MIGRATIONS
|
||||||
|
run: |
|
||||||
|
bin/rails db:drop
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:prepare
|
||||||
|
bin/rails db:migrate
|
||||||
|
|
||||||
- name: Test "one step migration" flow
|
- name: Test "one step migration" flow
|
||||||
run: |
|
run: |
|
||||||
bin/rails db:drop
|
bin/rails db:drop
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ docker-compose.override.yml
|
||||||
|
|
||||||
# Ignore emoji map file
|
# Ignore emoji map file
|
||||||
/app/javascript/mastodon/features/emoji/emoji_map.json
|
/app/javascript/mastodon/features/emoji/emoji_map.json
|
||||||
|
/app/javascript/mastodon/features/emoji/emoji_sheet.json
|
||||||
|
|
||||||
# Ignore locale files
|
# Ignore locale files
|
||||||
/app/javascript/mastodon/locales/*.json
|
/app/javascript/mastodon/locales/*.json
|
||||||
|
|
@ -86,6 +87,7 @@ AUTHORS.md
|
||||||
|
|
||||||
# Ignore glitch-soc emoji map file
|
# Ignore glitch-soc emoji map file
|
||||||
/app/javascript/flavours/glitch/features/emoji/emoji_map.json
|
/app/javascript/flavours/glitch/features/emoji/emoji_map.json
|
||||||
|
/app/javascript/flavours/glitch/features/emoji/emoji_sheet.json
|
||||||
|
|
||||||
# Ignore glitch-soc locale files
|
# Ignore glitch-soc locale files
|
||||||
/app/javascript/flavours/glitch/locales
|
/app/javascript/flavours/glitch/locales
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ inherit_from:
|
||||||
- .rubocop/rspec_rails.yml
|
- .rubocop/rspec_rails.yml
|
||||||
- .rubocop/rspec.yml
|
- .rubocop/rspec.yml
|
||||||
- .rubocop/style.yml
|
- .rubocop/style.yml
|
||||||
|
- .rubocop/i18n.yml
|
||||||
- .rubocop/custom.yml
|
- .rubocop/custom.yml
|
||||||
- .rubocop_todo.yml
|
- .rubocop_todo.yml
|
||||||
- .rubocop/strict.yml
|
- .rubocop/strict.yml
|
||||||
|
|
@ -27,10 +28,9 @@ inherit_mode:
|
||||||
- Exclude
|
- Exclude
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
|
- rubocop-capybara
|
||||||
|
- rubocop-i18n
|
||||||
|
- rubocop-performance
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
- rubocop-performance
|
|
||||||
|
|
||||||
require:
|
|
||||||
- rubocop-rspec_rails
|
- rubocop-rspec_rails
|
||||||
- rubocop-capybara
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
I18n/RailsI18n:
|
||||||
|
Enabled: true
|
||||||
|
Exclude:
|
||||||
|
- 'config/**/*'
|
||||||
|
- 'db/**/*'
|
||||||
|
- 'lib/**/*'
|
||||||
|
- 'spec/**/*'
|
||||||
|
I18n/GetText:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
I18n/RailsI18n/DecorateStringFormattingUsingInterpolation:
|
||||||
|
Enabled: false
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
||||||
# using RuboCop version 1.72.2.
|
# using RuboCop version 1.73.2.
|
||||||
# The point is for the user to remove these configuration records
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the offenses are removed from the code base.
|
# one by one as the offenses are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
|
|
|
||||||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -2,6 +2,30 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [4.3.6] - 2025-03-13
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update dependency `omniauth-saml`
|
||||||
|
- Update dependency `rack`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix Stoplight errors when using `REDIS_NAMESPACE` (#34126 by @ClearlyClaire)
|
||||||
|
|
||||||
|
## [4.3.5] - 2025-03-10
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change hashtag suggestion to prefer personal history capitalization (#34070 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix processing errors for some HEIF images from iOS 18 (#34086 by @renchap)
|
||||||
|
- Fix streaming server not filtering unknown-language posts from public timelines (#33774 by @ClearlyClaire)
|
||||||
|
- Fix preview cards under Content Warnings not being shown in detailed statuses (#34068 by @ClearlyClaire)
|
||||||
|
- Fix username and display name being hidden on narrow screens in moderation interface (#33064 by @ClearlyClaire)
|
||||||
|
|
||||||
## [4.3.4] - 2025-02-27
|
## [4.3.4] - 2025-02-27
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
|
||||||
65
Dockerfile
65
Dockerfile
|
|
@ -14,12 +14,12 @@ ARG BASE_REGISTRY="docker.io"
|
||||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
|
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
|
||||||
# renovate: datasource=docker depName=docker.io/ruby
|
# renovate: datasource=docker depName=docker.io/ruby
|
||||||
ARG RUBY_VERSION="3.4.2"
|
ARG RUBY_VERSION="3.4.2"
|
||||||
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||||
# renovate: datasource=node-version depName=node
|
# renovate: datasource=node-version depName=node
|
||||||
ARG NODE_MAJOR_VERSION="22"
|
ARG NODE_MAJOR_VERSION="22"
|
||||||
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
||||||
ARG DEBIAN_VERSION="bookworm"
|
ARG DEBIAN_VERSION="bookworm"
|
||||||
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
# Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
||||||
FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
|
FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
|
||||||
# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm)
|
# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm)
|
||||||
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
|
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
|
||||||
|
|
@ -61,7 +61,7 @@ ENV \
|
||||||
ENV \
|
ENV \
|
||||||
# Configure the IP to bind Mastodon to when serving traffic
|
# Configure the IP to bind Mastodon to when serving traffic
|
||||||
BIND="0.0.0.0" \
|
BIND="0.0.0.0" \
|
||||||
# Use production settings for Yarn, Node and related nodejs based tools
|
# Use production settings for Yarn, Node.js and related tools
|
||||||
NODE_ENV="production" \
|
NODE_ENV="production" \
|
||||||
# Use production settings for Ruby on Rails
|
# Use production settings for Ruby on Rails
|
||||||
RAILS_ENV="production" \
|
RAILS_ENV="production" \
|
||||||
|
|
@ -96,6 +96,9 @@ RUN \
|
||||||
# Set /opt/mastodon as working directory
|
# Set /opt/mastodon as working directory
|
||||||
WORKDIR /opt/mastodon
|
WORKDIR /opt/mastodon
|
||||||
|
|
||||||
|
# Add backport repository for some specific packages where we need the latest version
|
||||||
|
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
|
||||||
|
|
||||||
# hadolint ignore=DL3008,DL3005
|
# hadolint ignore=DL3008,DL3005
|
||||||
RUN \
|
RUN \
|
||||||
# Mount Apt cache and lib directories from Docker buildx caches
|
# Mount Apt cache and lib directories from Docker buildx caches
|
||||||
|
|
@ -125,13 +128,6 @@ RUN \
|
||||||
# Create temporary build layer from base image
|
# Create temporary build layer from base image
|
||||||
FROM ruby AS build
|
FROM ruby AS build
|
||||||
|
|
||||||
# Copy Node package configuration files into working directory
|
|
||||||
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
|
||||||
COPY .yarn /opt/mastodon/.yarn
|
|
||||||
|
|
||||||
COPY --from=node /usr/local/bin /usr/local/bin
|
|
||||||
COPY --from=node /usr/local/lib /usr/local/lib
|
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
# hadolint ignore=DL3008
|
# hadolint ignore=DL3008
|
||||||
|
|
@ -165,7 +161,7 @@ RUN \
|
||||||
libexif-dev \
|
libexif-dev \
|
||||||
libexpat1-dev \
|
libexpat1-dev \
|
||||||
libgirepository1.0-dev \
|
libgirepository1.0-dev \
|
||||||
libheif-dev \
|
libheif-dev/bookworm-backports \
|
||||||
libimagequant-dev \
|
libimagequant-dev \
|
||||||
libjpeg62-turbo-dev \
|
libjpeg62-turbo-dev \
|
||||||
liblcms2-dev \
|
liblcms2-dev \
|
||||||
|
|
@ -185,12 +181,6 @@ RUN \
|
||||||
libx265-dev \
|
libx265-dev \
|
||||||
;
|
;
|
||||||
|
|
||||||
RUN \
|
|
||||||
# Configure Corepack
|
|
||||||
rm /usr/local/bin/yarn*; \
|
|
||||||
corepack enable; \
|
|
||||||
corepack prepare --activate;
|
|
||||||
|
|
||||||
# Create temporary libvips specific build layer from build layer
|
# Create temporary libvips specific build layer from build layer
|
||||||
FROM build AS libvips
|
FROM build AS libvips
|
||||||
|
|
||||||
|
|
@ -281,38 +271,37 @@ RUN \
|
||||||
# Download and install required Gems
|
# Download and install required Gems
|
||||||
bundle install -j"$(nproc)";
|
bundle install -j"$(nproc)";
|
||||||
|
|
||||||
# Create temporary node specific build layer from build layer
|
# Create temporary assets build layer from build layer
|
||||||
FROM build AS yarn
|
FROM build AS precompiler
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
# Copy Node package configuration files into working directory
|
# Copy Mastodon sources into layer
|
||||||
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
COPY . /opt/mastodon/
|
||||||
COPY streaming/package.json /opt/mastodon/streaming/
|
|
||||||
COPY .yarn /opt/mastodon/.yarn
|
# Copy Node.js binaries/libraries into layer
|
||||||
|
COPY --from=node /usr/local/bin /usr/local/bin
|
||||||
|
COPY --from=node /usr/local/lib /usr/local/lib
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
# Configure Corepack
|
||||||
|
rm /usr/local/bin/yarn*; \
|
||||||
|
corepack enable; \
|
||||||
|
corepack prepare --activate;
|
||||||
|
|
||||||
# hadolint ignore=DL3008
|
# hadolint ignore=DL3008
|
||||||
RUN \
|
RUN \
|
||||||
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
|
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
|
||||||
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
|
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
|
||||||
# Install Node packages
|
# Install Node.js packages
|
||||||
yarn workspaces focus --production @mastodon/mastodon;
|
yarn workspaces focus --production @mastodon/mastodon;
|
||||||
|
|
||||||
# Create temporary assets build layer from build layer
|
# Copy libvips components into layer for precompiler
|
||||||
FROM build AS precompiler
|
|
||||||
|
|
||||||
# Copy Mastodon sources into precompiler layer
|
|
||||||
COPY . /opt/mastodon/
|
|
||||||
|
|
||||||
# Copy bundler and node packages from build layer to container
|
|
||||||
COPY --from=yarn /opt/mastodon /opt/mastodon/
|
|
||||||
COPY --from=bundler /opt/mastodon /opt/mastodon/
|
|
||||||
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
|
|
||||||
# Copy libvips components to layer for precompiler
|
|
||||||
COPY --from=libvips /usr/local/libvips/bin /usr/local/bin
|
COPY --from=libvips /usr/local/libvips/bin /usr/local/bin
|
||||||
COPY --from=libvips /usr/local/libvips/lib /usr/local/lib
|
COPY --from=libvips /usr/local/libvips/lib /usr/local/lib
|
||||||
|
# Copy bundler packages into layer for precompiler
|
||||||
ARG TARGETPLATFORM
|
COPY --from=bundler /opt/mastodon /opt/mastodon/
|
||||||
|
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
ldconfig; \
|
ldconfig; \
|
||||||
|
|
@ -348,7 +337,7 @@ RUN \
|
||||||
# libvips components
|
# libvips components
|
||||||
libcgif0 \
|
libcgif0 \
|
||||||
libexif12 \
|
libexif12 \
|
||||||
libheif1 \
|
libheif1/bookworm-backports \
|
||||||
libimagequant0 \
|
libimagequant0 \
|
||||||
libjpeg62-turbo \
|
libjpeg62-turbo \
|
||||||
liblcms2-2 \
|
liblcms2-2 \
|
||||||
|
|
|
||||||
7
Gemfile
7
Gemfile
|
|
@ -39,7 +39,7 @@ gem 'net-ldap', '~> 0.18'
|
||||||
|
|
||||||
gem 'omniauth', '~> 2.0'
|
gem 'omniauth', '~> 2.0'
|
||||||
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
||||||
gem 'omniauth_openid_connect', '~> 0.6.1'
|
gem 'omniauth_openid_connect', '~> 0.8.0'
|
||||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||||
gem 'omniauth-saml', '~> 2.0'
|
gem 'omniauth-saml', '~> 2.0'
|
||||||
|
|
||||||
|
|
@ -102,10 +102,10 @@ gem 'rdf-normalize', '~> 0.5'
|
||||||
|
|
||||||
gem 'prometheus_exporter', '~> 2.2', require: false
|
gem 'prometheus_exporter', '~> 2.2', require: false
|
||||||
|
|
||||||
gem 'opentelemetry-api', '~> 1.4.0'
|
gem 'opentelemetry-api', '~> 1.5.0'
|
||||||
|
|
||||||
group :opentelemetry do
|
group :opentelemetry do
|
||||||
gem 'opentelemetry-exporter-otlp', '~> 0.29.0', require: false
|
gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
|
gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
|
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
|
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
|
||||||
|
|
@ -165,6 +165,7 @@ group :development do
|
||||||
# Code linting CLI and plugins
|
# Code linting CLI and plugins
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
gem 'rubocop-capybara', require: false
|
gem 'rubocop-capybara', require: false
|
||||||
|
gem 'rubocop-i18n', require: false
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
gem 'rubocop-rails', require: false
|
gem 'rubocop-rails', require: false
|
||||||
gem 'rubocop-rspec', require: false
|
gem 'rubocop-rspec', require: false
|
||||||
|
|
|
||||||
233
Gemfile.lock
233
Gemfile.lock
|
|
@ -10,29 +10,29 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (8.0.1)
|
actioncable (8.0.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (8.0.1)
|
actionmailbox (8.0.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.0.2)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.0.2)
|
||||||
activestorage (= 8.0.1)
|
activestorage (= 8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (8.0.1)
|
actionmailer (8.0.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2)
|
||||||
actionview (= 8.0.1)
|
actionview (= 8.0.2)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (8.0.1)
|
actionpack (8.0.2)
|
||||||
actionview (= 8.0.1)
|
actionview (= 8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
|
|
@ -40,15 +40,15 @@ GEM
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
useragent (~> 0.16)
|
useragent (~> 0.16)
|
||||||
actiontext (8.0.1)
|
actiontext (8.0.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.0.2)
|
||||||
activestorage (= 8.0.1)
|
activestorage (= 8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (8.0.1)
|
actionview (8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
|
|
@ -58,22 +58,22 @@ GEM
|
||||||
activemodel (>= 4.1)
|
activemodel (>= 4.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (8.0.1)
|
activejob (8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (8.0.1)
|
activemodel (8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
activerecord (8.0.1)
|
activerecord (8.0.2)
|
||||||
activemodel (= 8.0.1)
|
activemodel (= 8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (8.0.1)
|
activestorage (8.0.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.0.2)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (8.0.1)
|
activesupport (8.0.2)
|
||||||
base64
|
base64
|
||||||
benchmark (>= 0.3)
|
benchmark (>= 0.3)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
|
|
@ -217,6 +217,8 @@ GEM
|
||||||
htmlentities (~> 4.3.3)
|
htmlentities (~> 4.3.3)
|
||||||
launchy (>= 2.1, < 4.0)
|
launchy (>= 2.1, < 4.0)
|
||||||
mail (~> 2.7)
|
mail (~> 2.7)
|
||||||
|
email_validator (2.2.4)
|
||||||
|
activemodel
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
|
|
@ -228,6 +230,8 @@ GEM
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
json
|
json
|
||||||
logger
|
logger
|
||||||
|
faraday-follow_redirects (0.3.0)
|
||||||
|
faraday (>= 1, < 3)
|
||||||
faraday-httpclient (2.0.1)
|
faraday-httpclient (2.0.1)
|
||||||
httpclient (>= 2.2)
|
httpclient (>= 2.2)
|
||||||
faraday-net_http (3.4.0)
|
faraday-net_http (3.4.0)
|
||||||
|
|
@ -261,8 +265,8 @@ GEM
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-protobuf (3.25.5)
|
google-protobuf (3.25.6)
|
||||||
googleapis-common-protos-types (1.15.0)
|
googleapis-common-protos-types (1.18.0)
|
||||||
google-protobuf (>= 3.18, < 5.a)
|
google-protobuf (>= 3.18, < 5.a)
|
||||||
haml (6.3.0)
|
haml (6.3.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
|
|
@ -304,7 +308,7 @@ GEM
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (1.0.14)
|
i18n-tasks (1.0.15)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
erubi
|
erubi
|
||||||
|
|
@ -313,6 +317,7 @@ GEM
|
||||||
parser (>= 3.2.2.1)
|
parser (>= 3.2.2.1)
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
ruby-progressbar (~> 1.8, >= 1.8.1)
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
idn-ruby (0.1.5)
|
idn-ruby (0.1.5)
|
||||||
inline_svg (1.10.0)
|
inline_svg (1.10.0)
|
||||||
|
|
@ -328,13 +333,15 @@ GEM
|
||||||
azure-blob (~> 0.5.2)
|
azure-blob (~> 0.5.2)
|
||||||
hashie (~> 5.0)
|
hashie (~> 5.0)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.10.1)
|
json (2.10.2)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.15.3.1)
|
json-jwt (1.16.7)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
aes_key_wrap
|
aes_key_wrap
|
||||||
|
base64
|
||||||
bindata
|
bindata
|
||||||
httpclient
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
json-ld (3.3.2)
|
json-ld (3.3.2)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
json-canonicalization (~> 1.0)
|
json-canonicalization (~> 1.0)
|
||||||
|
|
@ -435,41 +442,43 @@ GEM
|
||||||
oj (3.16.10)
|
oj (3.16.10)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
omniauth (2.1.2)
|
omniauth (2.1.3)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
omniauth-cas (3.0.0)
|
omniauth-cas (3.0.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
nokogiri (~> 1.12)
|
nokogiri (~> 1.12)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
omniauth-rails_csrf_protection (1.0.2)
|
omniauth-rails_csrf_protection (1.0.2)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
omniauth-saml (2.2.1)
|
omniauth-saml (2.2.3)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
ruby-saml (~> 1.17)
|
ruby-saml (~> 1.18)
|
||||||
omniauth_openid_connect (0.6.1)
|
omniauth_openid_connect (0.8.0)
|
||||||
omniauth (>= 1.9, < 3)
|
omniauth (>= 1.9, < 3)
|
||||||
openid_connect (~> 1.1)
|
openid_connect (~> 2.2)
|
||||||
openid_connect (1.4.2)
|
openid_connect (2.3.1)
|
||||||
activemodel
|
activemodel
|
||||||
attr_required (>= 1.0.0)
|
attr_required (>= 1.0.0)
|
||||||
json-jwt (>= 1.15.0)
|
email_validator
|
||||||
net-smtp
|
faraday (~> 2.0)
|
||||||
rack-oauth2 (~> 1.21)
|
faraday-follow_redirects
|
||||||
swd (~> 1.3)
|
json-jwt (>= 1.16)
|
||||||
|
mail
|
||||||
|
rack-oauth2 (~> 2.2)
|
||||||
|
swd (~> 2.0)
|
||||||
tzinfo
|
tzinfo
|
||||||
validate_email
|
|
||||||
validate_url
|
validate_url
|
||||||
webfinger (~> 1.2)
|
webfinger (~> 2.0)
|
||||||
openssl (3.3.0)
|
openssl (3.3.0)
|
||||||
openssl-signature_algorithm (1.3.0)
|
openssl-signature_algorithm (1.3.0)
|
||||||
openssl (> 2.0)
|
openssl (> 2.0)
|
||||||
opentelemetry-api (1.4.0)
|
opentelemetry-api (1.5.0)
|
||||||
opentelemetry-common (0.21.0)
|
opentelemetry-common (0.22.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-exporter-otlp (0.29.1)
|
opentelemetry-exporter-otlp (0.30.0)
|
||||||
google-protobuf (>= 3.18)
|
google-protobuf (>= 3.18)
|
||||||
googleapis-common-protos-types (~> 1.3)
|
googleapis-common-protos-types (~> 1.3)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
|
|
@ -500,8 +509,8 @@ GEM
|
||||||
opentelemetry-instrumentation-active_record (0.9.0)
|
opentelemetry-instrumentation-active_record (0.9.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-active_storage (0.1.0)
|
opentelemetry-instrumentation-active_storage (0.1.1)
|
||||||
opentelemetry-api (~> 1.4.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.7)
|
opentelemetry-instrumentation-active_support (~> 0.7)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-active_support (0.8.0)
|
opentelemetry-instrumentation-active_support (0.8.0)
|
||||||
|
|
@ -553,14 +562,14 @@ GEM
|
||||||
opentelemetry-instrumentation-sidekiq (0.26.0)
|
opentelemetry-instrumentation-sidekiq (0.26.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-registry (0.3.1)
|
opentelemetry-registry (0.4.0)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-sdk (1.7.0)
|
opentelemetry-sdk (1.8.0)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-common (~> 0.20)
|
opentelemetry-common (~> 0.20)
|
||||||
opentelemetry-registry (~> 0.2)
|
opentelemetry-registry (~> 0.2)
|
||||||
opentelemetry-semantic_conventions
|
opentelemetry-semantic_conventions
|
||||||
opentelemetry-semantic_conventions (1.10.1)
|
opentelemetry-semantic_conventions (1.11.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.1)
|
||||||
|
|
@ -600,19 +609,20 @@ GEM
|
||||||
public_suffix (6.0.1)
|
public_suffix (6.0.1)
|
||||||
puma (6.6.0)
|
puma (6.6.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.4.0)
|
pundit (2.5.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (2.2.11)
|
rack (2.2.13)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.7.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
rack-cors (2.0.2)
|
rack-cors (2.0.2)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-oauth2 (1.21.3)
|
rack-oauth2 (2.2.1)
|
||||||
activesupport
|
activesupport
|
||||||
attr_required
|
attr_required
|
||||||
httpclient
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
json-jwt (>= 1.11.0)
|
json-jwt (>= 1.11.0)
|
||||||
rack (>= 2.1.0)
|
rack (>= 2.1.0)
|
||||||
rack-protection (3.2.0)
|
rack-protection (3.2.0)
|
||||||
|
|
@ -627,20 +637,20 @@ GEM
|
||||||
rackup (1.0.1)
|
rackup (1.0.1)
|
||||||
rack (< 3)
|
rack (< 3)
|
||||||
webrick
|
webrick
|
||||||
rails (8.0.1)
|
rails (8.0.2)
|
||||||
actioncable (= 8.0.1)
|
actioncable (= 8.0.2)
|
||||||
actionmailbox (= 8.0.1)
|
actionmailbox (= 8.0.2)
|
||||||
actionmailer (= 8.0.1)
|
actionmailer (= 8.0.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2)
|
||||||
actiontext (= 8.0.1)
|
actiontext (= 8.0.2)
|
||||||
actionview (= 8.0.1)
|
actionview (= 8.0.2)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.0.2)
|
||||||
activemodel (= 8.0.1)
|
activemodel (= 8.0.2)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.0.2)
|
||||||
activestorage (= 8.0.1)
|
activestorage (= 8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.1)
|
railties (= 8.0.2)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
|
|
@ -651,9 +661,9 @@ GEM
|
||||||
rails-i18n (8.0.1)
|
rails-i18n (8.0.1)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 8.0.0, < 9)
|
railties (>= 8.0.0, < 9)
|
||||||
railties (8.0.1)
|
railties (8.0.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2)
|
||||||
irb (~> 1.13)
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
|
|
@ -669,7 +679,7 @@ GEM
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
rdoc (6.12.0)
|
rdoc (6.12.0)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redcarpet (3.6.0)
|
redcarpet (3.6.1)
|
||||||
redis (4.8.1)
|
redis (4.8.1)
|
||||||
redis-namespace (1.11.0)
|
redis-namespace (1.11.0)
|
||||||
redis (>= 4)
|
redis (>= 4)
|
||||||
|
|
@ -713,13 +723,13 @@ GEM
|
||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (~> 3.13)
|
||||||
rspec-sidekiq (5.0.0)
|
rspec-sidekiq (5.1.0)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
rspec-expectations (~> 3.0)
|
rspec-expectations (~> 3.0)
|
||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 9)
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.2)
|
||||||
rubocop (1.72.2)
|
rubocop (1.73.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
|
|
@ -730,15 +740,19 @@ GEM
|
||||||
rubocop-ast (>= 1.38.0, < 2.0)
|
rubocop-ast (>= 1.38.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.38.0)
|
rubocop-ast (1.38.1)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-capybara (2.21.0)
|
rubocop-capybara (2.22.1)
|
||||||
rubocop (~> 1.41)
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
|
rubocop-i18n (3.2.3)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (>= 1.72.1)
|
||||||
rubocop-performance (1.24.0)
|
rubocop-performance (1.24.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (>= 1.72.1, < 2.0)
|
rubocop (>= 1.72.1, < 2.0)
|
||||||
rubocop-ast (>= 1.38.0, < 2.0)
|
rubocop-ast (>= 1.38.0, < 2.0)
|
||||||
rubocop-rails (2.30.2)
|
rubocop-rails (2.30.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
|
|
@ -747,12 +761,13 @@ GEM
|
||||||
rubocop-rspec (3.5.0)
|
rubocop-rspec (3.5.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (~> 1.72, >= 1.72.1)
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
rubocop-rspec_rails (2.30.0)
|
rubocop-rspec_rails (2.31.0)
|
||||||
rubocop (~> 1.61)
|
lint_roller (~> 1.1)
|
||||||
rubocop-rspec (~> 3, >= 3.0.1)
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
|
rubocop-rspec (~> 3.5)
|
||||||
ruby-prof (1.7.1)
|
ruby-prof (1.7.1)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-saml (1.17.0)
|
ruby-saml (1.18.0)
|
||||||
nokogiri (>= 1.13.10)
|
nokogiri (>= 1.13.10)
|
||||||
rexml
|
rexml
|
||||||
ruby-vips (2.2.3)
|
ruby-vips (2.2.3)
|
||||||
|
|
@ -810,13 +825,14 @@ GEM
|
||||||
stackprof (0.2.27)
|
stackprof (0.2.27)
|
||||||
stoplight (4.1.1)
|
stoplight (4.1.1)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
stringio (3.1.4)
|
stringio (3.1.5)
|
||||||
strong_migrations (2.2.0)
|
strong_migrations (2.2.0)
|
||||||
activerecord (>= 7)
|
activerecord (>= 7)
|
||||||
swd (1.3.0)
|
swd (2.0.3)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
attr_required (>= 0.0.5)
|
attr_required (>= 0.0.5)
|
||||||
httpclient (>= 2.4)
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.10.3)
|
temple (0.10.3)
|
||||||
terminal-table (4.0.0)
|
terminal-table (4.0.0)
|
||||||
|
|
@ -854,11 +870,8 @@ GEM
|
||||||
unicode-display_width (3.1.4)
|
unicode-display_width (3.1.4)
|
||||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||||
unicode-emoji (4.0.4)
|
unicode-emoji (4.0.4)
|
||||||
uri (1.0.2)
|
uri (1.0.3)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
validate_email (0.1.6)
|
|
||||||
activemodel (>= 3.0)
|
|
||||||
mail (>= 2.2.5)
|
|
||||||
validate_url (1.0.15)
|
validate_url (1.0.15)
|
||||||
activemodel (>= 3.0.0)
|
activemodel (>= 3.0.0)
|
||||||
public_suffix
|
public_suffix
|
||||||
|
|
@ -872,10 +885,11 @@ GEM
|
||||||
openssl (>= 2.2)
|
openssl (>= 2.2)
|
||||||
safety_net_attestation (~> 0.4.0)
|
safety_net_attestation (~> 0.4.0)
|
||||||
tpm-key_attestation (~> 0.14.0)
|
tpm-key_attestation (~> 0.14.0)
|
||||||
webfinger (1.2.0)
|
webfinger (2.1.3)
|
||||||
activesupport
|
activesupport
|
||||||
httpclient (>= 2.4)
|
faraday (~> 2.0)
|
||||||
webmock (3.25.0)
|
faraday-follow_redirects
|
||||||
|
webmock (3.25.1)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
|
@ -973,9 +987,9 @@ DEPENDENCIES
|
||||||
omniauth-cas (~> 3.0.0.beta.1)
|
omniauth-cas (~> 3.0.0.beta.1)
|
||||||
omniauth-rails_csrf_protection (~> 1.0)
|
omniauth-rails_csrf_protection (~> 1.0)
|
||||||
omniauth-saml (~> 2.0)
|
omniauth-saml (~> 2.0)
|
||||||
omniauth_openid_connect (~> 0.6.1)
|
omniauth_openid_connect (~> 0.8.0)
|
||||||
opentelemetry-api (~> 1.4.0)
|
opentelemetry-api (~> 1.5.0)
|
||||||
opentelemetry-exporter-otlp (~> 0.29.0)
|
opentelemetry-exporter-otlp (~> 0.30.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
||||||
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
|
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
|
||||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
|
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
|
||||||
|
|
@ -1016,6 +1030,7 @@ DEPENDENCIES
|
||||||
rspec-sidekiq (~> 5.0)
|
rspec-sidekiq (~> 5.0)
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-capybara
|
rubocop-capybara
|
||||||
|
rubocop-i18n
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Announcements::DistributionsController < Admin::BaseController
|
||||||
|
before_action :set_announcement
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize @announcement, :distribute?
|
||||||
|
@announcement.touch(:notification_sent_at)
|
||||||
|
Admin::DistributeAnnouncementNotificationWorker.perform_async(@announcement.id)
|
||||||
|
redirect_to admin_announcements_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_announcement
|
||||||
|
@announcement = Announcement.find(params[:announcement_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Announcements::PreviewsController < Admin::BaseController
|
||||||
|
before_action :set_announcement
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @announcement, :distribute?
|
||||||
|
@user_count = @announcement.scope_for_notification.count
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_announcement
|
||||||
|
@announcement = Announcement.find(params[:announcement_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Announcements::TestsController < Admin::BaseController
|
||||||
|
before_action :set_announcement
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize @announcement, :distribute?
|
||||||
|
UserMailer.announcement_published(current_user, @announcement).deliver_later!
|
||||||
|
redirect_to admin_announcements_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_announcement
|
||||||
|
@announcement = Announcement.find(params[:announcement_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -23,7 +23,7 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_terms_of_service
|
def set_terms_of_service
|
||||||
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text)
|
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now)
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_terms_of_service
|
def current_terms_of_service
|
||||||
|
|
@ -32,6 +32,6 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params
|
params
|
||||||
.expect(terms_of_service: [:text, :changelog])
|
.expect(terms_of_service: [:text, :changelog, :effective_date])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
class Admin::TermsOfServiceController < Admin::BaseController
|
class Admin::TermsOfServiceController < Admin::BaseController
|
||||||
def index
|
def index
|
||||||
authorize :terms_of_service, :index?
|
authorize :terms_of_service, :index?
|
||||||
@terms_of_service = TermsOfService.live.first
|
@terms_of_service = TermsOfService.published.first
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
||||||
@account = current_account
|
@account = current_account
|
||||||
UpdateAccountService.new.call(@account, account_params, raise_error: true)
|
UpdateAccountService.new.call(@account, account_params, raise_error: true)
|
||||||
current_user.update(user_params) if user_params
|
current_user.update(user_params) if user_params
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
render json: ValidationErrorFormatter.new(e).as_json, status: 422
|
render json: ValidationErrorFormatter.new(e).as_json, status: 422
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,18 @@ class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseCo
|
||||||
|
|
||||||
def show
|
def show
|
||||||
cache_even_if_authenticated!
|
cache_even_if_authenticated!
|
||||||
render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer
|
render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_terms_of_service
|
def set_terms_of_service
|
||||||
@terms_of_service = TermsOfService.live.first!
|
@terms_of_service = begin
|
||||||
|
if params[:date].present?
|
||||||
|
TermsOfService.published.find_by!(effective_date: params[:date])
|
||||||
|
else
|
||||||
|
TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
class Api::V1::MediaController < Api::BaseController
|
class Api::V1::MediaController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:media' }
|
before_action -> { doorkeeper_authorize! :write, :'write:media' }
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_media_attachment, except: [:create]
|
before_action :set_media_attachment, except: [:create, :destroy]
|
||||||
before_action :check_processing, except: [:create]
|
before_action :check_processing, except: [:create, :destroy]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
||||||
|
|
@ -25,6 +25,15 @@ class Api::V1::MediaController < Api::BaseController
|
||||||
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@media_attachment = current_account.media_attachments.find(params[:id])
|
||||||
|
|
||||||
|
return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil?
|
||||||
|
|
||||||
|
@media_attachment.destroy
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def status_code_for_media_attachment
|
def status_code_for_media_attachment
|
||||||
|
|
@ -54,4 +63,8 @@ class Api::V1::MediaController < Api::BaseController
|
||||||
def processing_error
|
def processing_error
|
||||||
{ error: 'Error processing thumbnail for uploaded media' }
|
{ error: 'Error processing thumbnail for uploaded media' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def in_usage_error
|
||||||
|
{ error: 'Media attachment is currently used by a status' }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController
|
||||||
def destroy
|
def destroy
|
||||||
@account = current_account
|
@account = current_account
|
||||||
UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
|
UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController
|
||||||
def destroy
|
def destroy
|
||||||
@account = current_account
|
@account = current_account
|
||||||
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
|
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
statuses = [@status] + @context.ancestors + @context.descendants
|
statuses = [@status] + @context.ancestors + @context.descendants
|
||||||
|
|
||||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||||
|
|
||||||
|
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies?
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
@ -113,7 +115,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
@status.account.statuses_count = @status.account.statuses_count - 1
|
@status.account.statuses_count = @status.account.statuses_count - 1
|
||||||
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||||
|
|
||||||
RemovalWorker.perform_async(@status.id, { 'redraft' => true })
|
RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
|
||||||
|
|
||||||
render json: json
|
render json: json
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ module Settings
|
||||||
def destroy
|
def destroy
|
||||||
if valid_picture?
|
if valid_picture?
|
||||||
if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
|
if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
|
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
|
||||||
else
|
else
|
||||||
redirect_to settings_profile_path
|
redirect_to settings_profile_path
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController
|
||||||
def update
|
def update
|
||||||
if UpdateAccountService.new.call(@account, account_params.except(:settings))
|
if UpdateAccountService.new.call(@account, account_params.except(:settings))
|
||||||
current_user.update!(settings_attributes: account_params[:settings])
|
current_user.update!(settings_attributes: account_params[:settings])
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if UpdateAccountService.new.call(@account, account_params)
|
if UpdateAccountService.new.call(@account, account_params)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
@account.build_fields
|
@account.build_fields
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ class Settings::VerificationsController < Settings::BaseController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if UpdateAccountService.new.call(@account, account_params)
|
if UpdateAccountService.new.call(@account, account_params)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
|
|
|
||||||
|
|
@ -155,24 +155,49 @@ module JsonLdHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
|
# Fetch the resource given by uri.
|
||||||
|
# @param uri [String]
|
||||||
|
# @param id_is_known [Boolean]
|
||||||
|
# @param on_behalf_of [nil, Account]
|
||||||
|
# @param raise_on_error [Symbol<:all, :temporary, :none>] See {#fetch_resource_without_id_validation} for possible values
|
||||||
|
def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: :none, request_options: {})
|
||||||
unless id_is_known
|
unless id_is_known
|
||||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error)
|
||||||
|
|
||||||
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
||||||
|
|
||||||
uri = json['id']
|
uri = json['id']
|
||||||
end
|
end
|
||||||
|
|
||||||
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
|
json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error, request_options: request_options)
|
||||||
json.present? && json['id'] == uri ? json : nil
|
json.present? && json['id'] == uri ? json : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
|
# Fetch the resource given by uri
|
||||||
|
#
|
||||||
|
# If an error is raised, it contains the response and can be captured for handling like
|
||||||
|
#
|
||||||
|
# begin
|
||||||
|
# fetch_resource_without_id_validation(uri, nil, true)
|
||||||
|
# rescue Mastodon::UnexpectedResponseError => e
|
||||||
|
# e.response
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# @param uri [String]
|
||||||
|
# @param on_behalf_of [nil, Account]
|
||||||
|
# @param raise_on_error [Symbol<:all, :temporary, :none>]
|
||||||
|
# - +:all+ - raise if response code is not in the 2xx range
|
||||||
|
# - +:temporary+ - raise if the response code is not an "unsalvageable error" like a 404
|
||||||
|
# (see {#response_error_unsalvageable} )
|
||||||
|
# - +:none+ - do not raise, return +nil+
|
||||||
|
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: :none, request_options: {})
|
||||||
on_behalf_of ||= Account.representative
|
on_behalf_of ||= Account.representative
|
||||||
|
|
||||||
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
||||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
raise Mastodon::UnexpectedResponseError, response if !response_successful?(response) && (
|
||||||
|
raise_on_error == :all ||
|
||||||
|
(!response_error_unsalvageable?(response) && raise_on_error == :temporary)
|
||||||
|
)
|
||||||
|
|
||||||
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
|
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import './public-path';
|
import './public-path';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
|
import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
|
||||||
|
|
||||||
import { start } from '../mastodon/common';
|
import { start } from '../mastodon/common';
|
||||||
import { Status } from '../mastodon/features/standalone/status';
|
import { Status } from '../mastodon/features/standalone/status';
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,13 @@ export function fetchAccountFail(id, error) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {boolean} [options.reblogs]
|
||||||
|
* @param {boolean} [options.notify]
|
||||||
|
* @returns {function(): void}
|
||||||
|
*/
|
||||||
export function followAccount(id, options = { reblogs: true }) {
|
export function followAccount(id, options = { reblogs: true }) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => {
|
||||||
api().put('/api/web/settings', { data })
|
api().put('/api/web/settings', { data })
|
||||||
.then(() => dispatch({ type: SETTING_SAVE }))
|
.then(() => dispatch({ type: SETTING_SAVE }))
|
||||||
.catch(error => dispatch(showAlertForError(error)));
|
.catch(error => dispatch(showAlertForError(error)));
|
||||||
}, 5000, { trailing: true });
|
}, 2000, { leading: true, trailing: true });
|
||||||
|
|
||||||
export function saveSettings() {
|
export function saveSettings() {
|
||||||
return (dispatch, getState) => debouncedSave(dispatch, getState);
|
return (dispatch, getState) => debouncedSave(dispatch, getState);
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ export function deleteStatus(id, withRedraft = false) {
|
||||||
|
|
||||||
dispatch(deleteStatusRequest(id));
|
dispatch(deleteStatusRequest(id));
|
||||||
|
|
||||||
api().delete(`/api/v1/statuses/${id}`).then(response => {
|
api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
|
||||||
dispatch(deleteStatusSuccess(id));
|
dispatch(deleteStatusSuccess(id));
|
||||||
dispatch(deleteFromTimelines(id));
|
dispatch(deleteFromTimelines(id));
|
||||||
dispatch(importFetchedAccount(response.data.account));
|
dispatch(importFetchedAccount(response.data.account));
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,12 @@ import type {
|
||||||
ApiPrivacyPolicyJSON,
|
ApiPrivacyPolicyJSON,
|
||||||
} from 'flavours/glitch/api_types/instance';
|
} from 'flavours/glitch/api_types/instance';
|
||||||
|
|
||||||
export const apiGetTermsOfService = () =>
|
export const apiGetTermsOfService = (version?: string) =>
|
||||||
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
|
apiRequestGet<ApiTermsOfServiceJSON>(
|
||||||
|
version
|
||||||
|
? `v1/instance/terms_of_service/${version}`
|
||||||
|
: 'v1/instance/terms_of_service',
|
||||||
|
);
|
||||||
|
|
||||||
export const apiGetPrivacyPolicy = () =>
|
export const apiGetPrivacyPolicy = () =>
|
||||||
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
export interface ApiTermsOfServiceJSON {
|
export interface ApiTermsOfServiceJSON {
|
||||||
updated_at: string;
|
effective_date: string;
|
||||||
|
effective: boolean;
|
||||||
|
succeeded_by: string | null;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
UsePopperOptions,
|
UsePopperOptions,
|
||||||
} from 'react-overlays/esm/usePopper';
|
} from 'react-overlays/esm/usePopper';
|
||||||
|
|
||||||
import { useSelectableClick } from '@/hooks/useSelectableClick';
|
import { useSelectableClick } from 'flavours/glitch/hooks/useSelectableClick';
|
||||||
|
|
||||||
const offset = [0, 4] as OffsetValue;
|
const offset = [0, 4] as OffsetValue;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { useHovering } from 'flavours/glitch/hooks/useHovering';
|
||||||
|
import { autoPlayGif } from 'flavours/glitch/initial_state';
|
||||||
import type { Account } from 'flavours/glitch/models/account';
|
import type { Account } from 'flavours/glitch/models/account';
|
||||||
|
|
||||||
import { useHovering } from '../hooks/useHovering';
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export const FollowButton: React.FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relationship) return;
|
if (!relationship || !accountId) return;
|
||||||
|
|
||||||
if (accountId === me) {
|
if (accountId === me) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useHovering } from '@/hooks/useHovering';
|
import { useHovering } from 'flavours/glitch/hooks/useHovering';
|
||||||
import { autoPlayGif } from 'flavours/glitch/initial_state';
|
import { autoPlayGif } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
export const GIF: React.FC<{
|
export const GIF: React.FC<{
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
|
||||||
<Sparklines
|
<Sparklines
|
||||||
width={50}
|
width={50}
|
||||||
height={28}
|
height={28}
|
||||||
data={history ? history : Array.from(Array(7)).map(() => 0)}
|
data={history ?? Array.from(Array(7)).map(() => 0)}
|
||||||
>
|
>
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
</Sparklines>
|
</Sparklines>
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ export class IconButton extends PureComponent<Props, States> {
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ class ScrollableList extends PureComponent {
|
||||||
bindToDocument: PropTypes.bool,
|
bindToDocument: PropTypes.bool,
|
||||||
preventScroll: PropTypes.bool,
|
preventScroll: PropTypes.bool,
|
||||||
footer: PropTypes.node,
|
footer: PropTypes.node,
|
||||||
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
|
@ -325,7 +326,7 @@ class ScrollableList extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
const { children, scrollKey, className, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
||||||
const { fullscreen } = this.state;
|
const { fullscreen } = this.state;
|
||||||
const childrenCount = Children.count(children);
|
const childrenCount = Children.count(children);
|
||||||
|
|
||||||
|
|
@ -336,9 +337,9 @@ class ScrollableList extends PureComponent {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
scrollableArea = (
|
scrollableArea = (
|
||||||
<div className='scrollable scrollable--flex' ref={this.setRef}>
|
<div className='scrollable scrollable--flex' ref={this.setRef}>
|
||||||
<div role='feed' className='item-list'>
|
{prepend}
|
||||||
{prepend}
|
|
||||||
</div>
|
<div role='feed' className='item-list' />
|
||||||
|
|
||||||
<div className='scrollable__append'>
|
<div className='scrollable__append'>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
|
|
@ -350,9 +351,9 @@ class ScrollableList extends PureComponent {
|
||||||
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
||||||
scrollableArea = (
|
scrollableArea = (
|
||||||
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||||
<div role='feed' className='item-list'>
|
{prepend}
|
||||||
{prepend}
|
|
||||||
|
|
||||||
|
<div role='feed' className={classNames('item-list', className)}>
|
||||||
{loadPending}
|
{loadPending}
|
||||||
|
|
||||||
{Children.map(this.props.children, (child, index) => (
|
{Children.map(this.props.children, (child, index) => (
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
|
||||||
|
|
||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
|
|
||||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
|
|
||||||
|
|
||||||
class ActionBar extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
isStatusesPageActive = (match, location) => {
|
|
||||||
if (!match) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !location.pathname.match(/\/(followers|following)\/?$/);
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account } = this.props;
|
|
||||||
|
|
||||||
if (account.get('suspended')) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className='account__disclaimer'>
|
|
||||||
<Icon id='info-circle' icon={InfoIcon} />
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.suspended_disclaimer_full'
|
|
||||||
defaultMessage='This user has been suspended by a moderator.'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let extraInfo = '';
|
|
||||||
|
|
||||||
if (account.get('acct') !== account.get('username')) {
|
|
||||||
extraInfo = (
|
|
||||||
<div className='account__disclaimer'>
|
|
||||||
<Icon id='info-circle' icon={InfoIcon} />
|
|
||||||
<div>
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.disclaimer_full'
|
|
||||||
defaultMessage="Information below may reflect the user's profile incompletely."
|
|
||||||
/>
|
|
||||||
{' '}
|
|
||||||
<a target='_blank' rel='noopener' href={account.get('url')}>
|
|
||||||
<FormattedMessage id='account.view_full_profile' defaultMessage='View full profile' />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{extraInfo}
|
|
||||||
|
|
||||||
<div className='account__action-bar'>
|
|
||||||
<div className='account__action-bar-links'>
|
|
||||||
<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={`/@${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={`/@${account.get('acct')}/followers`}>
|
|
||||||
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
|
||||||
<strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ActionBar;
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import type { NavLinkProps } from 'react-router-dom';
|
||||||
|
|
||||||
|
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import type { Account } from 'flavours/glitch/models/account';
|
||||||
|
|
||||||
|
const isStatusesPageActive: NavLinkProps['isActive'] = (match, location) => {
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !/\/(followers|following)\/?$/.exec(location.pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionBar: React.FC<{ account: Account }> = ({ account }) => {
|
||||||
|
if (account.suspended) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='account__disclaimer'>
|
||||||
|
<Icon id='info-circle' icon={InfoIcon} />
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.suspended_disclaimer_full'
|
||||||
|
defaultMessage='This user has been suspended by a moderator.'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraInfo = null;
|
||||||
|
|
||||||
|
if (account.get('acct') !== account.get('username')) {
|
||||||
|
extraInfo = (
|
||||||
|
<div className='account__disclaimer'>
|
||||||
|
<Icon id='info-circle' icon={InfoIcon} />
|
||||||
|
<div>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.disclaimer_full'
|
||||||
|
defaultMessage="Information below may reflect the user's profile incompletely."
|
||||||
|
/>{' '}
|
||||||
|
<a target='_blank' rel='noopener' href={account.get('url')}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.view_full_profile'
|
||||||
|
defaultMessage='View full profile'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{extraInfo}
|
||||||
|
|
||||||
|
<div className='account__action-bar'>
|
||||||
|
<div className='account__action-bar-links'>
|
||||||
|
<NavLink
|
||||||
|
isActive={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={`/@${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={`/@${account.get('acct')}/followers`}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.followers'
|
||||||
|
defaultMessage='Followers'
|
||||||
|
/>
|
||||||
|
<strong>
|
||||||
|
{account.get('followers_count') < 0 ? (
|
||||||
|
'-'
|
||||||
|
) : (
|
||||||
|
<FormattedNumber value={account.get('followers_count')} />
|
||||||
|
)}
|
||||||
|
</strong>
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,415 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
|
||||||
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
|
|
||||||
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
|
|
||||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
|
||||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
|
||||||
import { Badge, AutomatedBadge, GroupBadge } from 'flavours/glitch/components/badge';
|
|
||||||
import { Button } from 'flavours/glitch/components/button';
|
|
||||||
import { CopyIconButton } from 'flavours/glitch/components/copy_icon_button';
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
|
||||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
|
||||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
|
||||||
import { autoPlayGif, me, domain as localDomain } from 'flavours/glitch/initial_state';
|
|
||||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
|
|
||||||
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
|
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
|
||||||
|
|
||||||
import AccountNoteContainer from '../containers/account_note_container';
|
|
||||||
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
|
|
||||||
|
|
||||||
import { DomainPill } from './domain_pill';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
|
||||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
|
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
|
||||||
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
|
||||||
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
|
||||||
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
|
|
||||||
direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' },
|
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
|
||||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
|
||||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
|
||||||
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
|
|
||||||
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
|
|
||||||
copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' },
|
|
||||||
media: { id: 'account.media', defaultMessage: 'Media' },
|
|
||||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
|
||||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
|
||||||
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
|
||||||
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
|
||||||
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
|
|
||||||
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
|
|
||||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
|
|
||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
|
||||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
|
|
||||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
|
||||||
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
|
|
||||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
|
||||||
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
|
|
||||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
|
||||||
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
|
|
||||||
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
|
|
||||||
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
|
||||||
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
|
|
||||||
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
|
|
||||||
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const titleFromAccount = account => {
|
|
||||||
const displayName = account.get('display_name');
|
|
||||||
const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${localDomain}` : account.get('acct');
|
|
||||||
const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
|
|
||||||
|
|
||||||
return `${prefix} (@${acct})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const dateFormatOptions = {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
};
|
|
||||||
|
|
||||||
class Header extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
identity: identityContextPropShape,
|
|
||||||
account: ImmutablePropTypes.record,
|
|
||||||
identity_props: ImmutablePropTypes.list,
|
|
||||||
onFollow: PropTypes.func.isRequired,
|
|
||||||
onBlock: PropTypes.func.isRequired,
|
|
||||||
onMention: PropTypes.func.isRequired,
|
|
||||||
onDirect: PropTypes.func.isRequired,
|
|
||||||
onReblogToggle: PropTypes.func.isRequired,
|
|
||||||
onNotifyToggle: PropTypes.func.isRequired,
|
|
||||||
onReport: PropTypes.func.isRequired,
|
|
||||||
onMute: PropTypes.func.isRequired,
|
|
||||||
onBlockDomain: PropTypes.func.isRequired,
|
|
||||||
onUnblockDomain: PropTypes.func.isRequired,
|
|
||||||
onEndorseToggle: PropTypes.func.isRequired,
|
|
||||||
onAddToList: PropTypes.func.isRequired,
|
|
||||||
onChangeLanguages: PropTypes.func.isRequired,
|
|
||||||
onInteractionModal: PropTypes.func.isRequired,
|
|
||||||
onOpenAvatar: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
domain: PropTypes.string.isRequired,
|
|
||||||
hidden: PropTypes.bool,
|
|
||||||
...WithRouterPropTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
openEditProfile = () => {
|
|
||||||
window.open(profileLink, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseEnter = ({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
|
||||||
|
|
||||||
for (var i = 0; i < emojis.length; i++) {
|
|
||||||
let emoji = emojis[i];
|
|
||||||
emoji.src = emoji.getAttribute('data-original');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseLeave = ({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
|
||||||
|
|
||||||
for (var i = 0; i < emojis.length; i++) {
|
|
||||||
let emoji = emojis[i];
|
|
||||||
emoji.src = emoji.getAttribute('data-static');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAvatarClick = e => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onOpenAvatar();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleShare = () => {
|
|
||||||
const { account } = this.props;
|
|
||||||
|
|
||||||
navigator.share({
|
|
||||||
url: account.get('url'),
|
|
||||||
}).catch((e) => {
|
|
||||||
if (e.name !== 'AbortError') console.error(e);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account, hidden, intl } = this.props;
|
|
||||||
const { signedIn, permissions } = this.props.identity;
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const suspended = account.get('suspended');
|
|
||||||
const isRemote = account.get('acct') !== account.get('username');
|
|
||||||
const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null;
|
|
||||||
|
|
||||||
let actionBtn, bellBtn, lockedIcon, shareBtn;
|
|
||||||
|
|
||||||
let info = [];
|
|
||||||
let menu = [];
|
|
||||||
|
|
||||||
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
|
|
||||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>);
|
|
||||||
} else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
|
|
||||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
|
|
||||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
|
|
||||||
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
|
|
||||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
|
|
||||||
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} iconComponent={account.getIn(['relationship', 'notifying']) ? NotificationsActiveIcon : NotificationsIcon} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('share' in navigator) {
|
|
||||||
shareBtn = <IconButton className='optional' iconComponent={ShareIcon} title={intl.formatMessage(messages.share, { name: account.get('username') })} onClick={this.handleShare} />;
|
|
||||||
} else {
|
|
||||||
shareBtn = <CopyIconButton className='optional' title={intl.formatMessage(messages.copy)} value={account.get('url')} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
|
||||||
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
|
||||||
actionBtn = <Button disabled><LoadingIndicator /></Button>;
|
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
|
||||||
actionBtn = <Button className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
|
|
||||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
|
||||||
}
|
|
||||||
} else if (profileLink) {
|
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
|
||||||
actionBtn = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.get('suspended') && !account.getIn(['relationship', 'following'])) {
|
|
||||||
actionBtn = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.get('locked')) {
|
|
||||||
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signedIn && account.get('id') !== me && !account.get('suspended')) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
|
|
||||||
menu.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRemote) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
|
|
||||||
menu.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.get('id') === me) {
|
|
||||||
if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
|
|
||||||
if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
|
||||||
menu.push(null);
|
|
||||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
|
||||||
menu.push(null);
|
|
||||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
|
||||||
} else if (signedIn) {
|
|
||||||
if (account.getIn(['relationship', 'following'])) {
|
|
||||||
if (!account.getIn(['relationship', 'muting'])) {
|
|
||||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
|
||||||
} else {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.languages), action: this.props.onChangeLanguages });
|
|
||||||
menu.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
|
|
||||||
menu.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.getIn(['relationship', 'muting'])) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
|
|
||||||
} else {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.getIn(['relationship', 'blocking'])) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
|
|
||||||
} else {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!account.get('suspended')) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signedIn && isRemote) {
|
|
||||||
menu.push(null);
|
|
||||||
|
|
||||||
if (account.getIn(['relationship', 'domain_blocking'])) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain });
|
|
||||||
} else {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.get('id') !== me && ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
|
|
||||||
menu.push(null);
|
|
||||||
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) });
|
|
||||||
}
|
|
||||||
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain }), href: `/admin/instances/${remoteDomain}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = { __html: account.get('note_emojified') };
|
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
|
||||||
const fields = account.get('fields');
|
|
||||||
const isLocal = account.get('acct').indexOf('@') === -1;
|
|
||||||
const username = account.get('acct').split('@')[0];
|
|
||||||
const domain = isLocal ? localDomain : account.get('acct').split('@')[1];
|
|
||||||
const isIndexable = !account.get('noindex');
|
|
||||||
|
|
||||||
const badges = [];
|
|
||||||
|
|
||||||
if (account.get('bot')) {
|
|
||||||
badges.push(<AutomatedBadge key='bot-badge' />);
|
|
||||||
} else if (account.get('group')) {
|
|
||||||
badges.push(<GroupBadge key='group-badge' />);
|
|
||||||
}
|
|
||||||
|
|
||||||
account.get('roles', []).forEach((role) => {
|
|
||||||
badges.push(<Badge key={`role-badge-${role.get('id')}`} label={<span>{role.get('name')}</span>} domain={domain} roleId={role.get('id')} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
|
||||||
{!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />}
|
|
||||||
|
|
||||||
<div className='account__header__image'>
|
|
||||||
<div className='account__header__info'>
|
|
||||||
{info}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='account__header__bar'>
|
|
||||||
<div className='account__header__tabs'>
|
|
||||||
<a className='avatar' href={account.get('avatar')} rel='noopener' target='_blank' onClick={this.handleAvatarClick}>
|
|
||||||
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div className='account__header__tabs__buttons'>
|
|
||||||
{!hidden && bellBtn}
|
|
||||||
{!hidden && shareBtn}
|
|
||||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
|
||||||
{!hidden && actionBtn}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='account__header__tabs__name'>
|
|
||||||
<h1>
|
|
||||||
<span dangerouslySetInnerHTML={displayNameHtml} />
|
|
||||||
<small>
|
|
||||||
<span>@{username}<span className='invisible'>@{domain}</span></span>
|
|
||||||
<DomainPill username={username} domain={domain} isSelf={me === account.get('id')} />
|
|
||||||
{lockedIcon}
|
|
||||||
</small>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{badges.length > 0 && (
|
|
||||||
<div className='account__header__badges'>
|
|
||||||
{badges}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!(suspended || hidden) && (
|
|
||||||
<div className='account__header__extra'>
|
|
||||||
<div className='account__header__bio'>
|
|
||||||
{(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />}
|
|
||||||
|
|
||||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
|
|
||||||
|
|
||||||
<div className='account__header__fields'>
|
|
||||||
<dl>
|
|
||||||
<dt><FormattedMessage id='account.joined_short' defaultMessage='Joined' /></dt>
|
|
||||||
<dd>{intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' })}</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
{fields.map((pair, i) => (
|
|
||||||
<dl key={i} className={classNames({ verified: pair.get('verified_at') })}>
|
|
||||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
|
|
||||||
|
|
||||||
<dd className='translate' title={pair.get('value_plain')}>
|
|
||||||
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' icon={CheckIcon} className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Helmet>
|
|
||||||
<title>{titleFromAccount(account)}</title>
|
|
||||||
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
|
||||||
<link rel='canonical' href={account.get('url')} />
|
|
||||||
</Helmet>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(withIdentity(injectIntl(Header)));
|
|
||||||
|
|
@ -15,11 +15,15 @@ import {
|
||||||
useBlurhash,
|
useBlurhash,
|
||||||
} from 'flavours/glitch/initial_state';
|
} from 'flavours/glitch/initial_state';
|
||||||
import type { Status, MediaAttachment } from 'flavours/glitch/models/status';
|
import type { Status, MediaAttachment } from 'flavours/glitch/models/status';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
export const MediaItem: React.FC<{
|
export const MediaItem: React.FC<{
|
||||||
attachment: MediaAttachment;
|
attachment: MediaAttachment;
|
||||||
onOpenMedia: (arg0: MediaAttachment) => void;
|
onOpenMedia: (arg0: MediaAttachment) => void;
|
||||||
}> = ({ attachment, onOpenMedia }) => {
|
}> = ({ attachment, onOpenMedia }) => {
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
state.accounts.get(attachment.getIn(['status', 'account']) as string),
|
||||||
|
);
|
||||||
const [visible, setVisible] = useState(
|
const [visible, setVisible] = useState(
|
||||||
(displayMedia !== 'hide_all' &&
|
(displayMedia !== 'hide_all' &&
|
||||||
!attachment.getIn(['status', 'sensitive'])) ||
|
!attachment.getIn(['status', 'sensitive'])) ||
|
||||||
|
|
@ -70,7 +74,7 @@ export const MediaItem: React.FC<{
|
||||||
attachment.get('description')) as string | undefined;
|
attachment.get('description')) as string | undefined;
|
||||||
const previewUrl = attachment.get('preview_url') as string;
|
const previewUrl = attachment.get('preview_url') as string;
|
||||||
const fullUrl = attachment.get('url') as string;
|
const fullUrl = attachment.get('url') as string;
|
||||||
const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
|
const avatarUrl = account?.avatar_static;
|
||||||
const lang = status.get('language') as string;
|
const lang = status.get('language') as string;
|
||||||
const blurhash = attachment.get('blurhash') as string;
|
const blurhash = attachment.get('blurhash') as string;
|
||||||
const statusUrl = status.get('url') as string;
|
const statusUrl = status.get('url') as string;
|
||||||
|
|
|
||||||
|
|
@ -1,239 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
|
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
|
||||||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
|
||||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
|
||||||
import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
|
|
||||||
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
|
||||||
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
|
||||||
import { getAccountGallery } from 'flavours/glitch/selectors';
|
|
||||||
|
|
||||||
import { expandAccountMediaTimeline } from '../../actions/timelines';
|
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
|
||||||
import Column from '../ui/components/column';
|
|
||||||
|
|
||||||
import { MediaItem } from './components/media_item';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
|
||||||
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(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 {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
maxId: PropTypes.string,
|
|
||||||
onLoadMore: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLoadMore = () => {
|
|
||||||
this.props.onLoadMore(this.props.maxId);
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<LoadMore
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
onClick={this.handleLoadMore}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class AccountGallery extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
params: PropTypes.shape({
|
|
||||||
acct: PropTypes.string,
|
|
||||||
id: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
accountId: PropTypes.string,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
attachments: ImmutablePropTypes.list.isRequired,
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
hasMore: PropTypes.bool,
|
|
||||||
isAccount: PropTypes.bool,
|
|
||||||
suspended: PropTypes.bool,
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
width: 323,
|
|
||||||
};
|
|
||||||
|
|
||||||
_load () {
|
|
||||||
const { accountId, isAccount, dispatch } = this.props;
|
|
||||||
|
|
||||||
if (!isAccount) dispatch(fetchAccount(accountId));
|
|
||||||
dispatch(expandAccountMediaTimeline(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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleHeaderClick = () => {
|
|
||||||
this.column.scrollTop();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleScrollToBottom = () => {
|
|
||||||
if (this.props.hasMore) {
|
|
||||||
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleScroll = e => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
|
||||||
const offset = scrollHeight - scrollTop - clientHeight;
|
|
||||||
|
|
||||||
if (150 > offset && !this.props.isLoading) {
|
|
||||||
this.handleScrollToBottom();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLoadMore = maxId => {
|
|
||||||
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLoadOlder = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.handleScrollToBottom();
|
|
||||||
};
|
|
||||||
|
|
||||||
setColumnRef = c => {
|
|
||||||
this.column = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOpenMedia = attachment => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const statusId = attachment.getIn(['status', 'id']);
|
|
||||||
const lang = attachment.getIn(['status', 'language']);
|
|
||||||
|
|
||||||
if (attachment.get('type') === 'video') {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'VIDEO',
|
|
||||||
modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
|
|
||||||
}));
|
|
||||||
} else if (attachment.get('type') === 'audio') {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'AUDIO',
|
|
||||||
modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
const media = attachment.getIn(['status', 'media_attachments']);
|
|
||||||
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
|
|
||||||
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'MEDIA',
|
|
||||||
modalProps: { media, index, statusId, lang },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRef = c => {
|
|
||||||
if (c) {
|
|
||||||
this.setState({ width: c.offsetWidth });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { attachments, isLoading, hasMore, isAccount, multiColumn, suspended } = this.props;
|
|
||||||
const { width } = this.state;
|
|
||||||
|
|
||||||
if (!isAccount) {
|
|
||||||
return (
|
|
||||||
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!attachments && isLoading) {
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadOlder = null;
|
|
||||||
|
|
||||||
if (hasMore && !(isLoading && attachments.size === 0)) {
|
|
||||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column ref={this.setColumnRef}>
|
|
||||||
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
|
|
||||||
|
|
||||||
<ScrollContainer scrollKey='account_gallery'>
|
|
||||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
|
||||||
<HeaderContainer accountId={this.props.accountId} />
|
|
||||||
|
|
||||||
{suspended ? (
|
|
||||||
<div className='empty-column-indicator'>
|
|
||||||
<FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
|
||||||
{attachments.map((attachment, index) => attachment === null ? (
|
|
||||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
|
||||||
) : (
|
|
||||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{loadOlder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoading && attachments.size === 0 && (
|
|
||||||
<div className='scrollable__append'>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollContainer>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(AccountGallery);
|
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
import { useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
|
||||||
|
import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||||
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
|
import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
|
||||||
|
import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header';
|
||||||
|
import { LimitedAccountHint } from 'flavours/glitch/features/account_timeline/components/limited_account_hint';
|
||||||
|
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
||||||
|
import Column from 'flavours/glitch/features/ui/components/column';
|
||||||
|
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
|
||||||
|
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
||||||
|
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
|
||||||
|
import type { RootState } from 'flavours/glitch/store';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { MediaItem } from './components/media_item';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
profile: { id: 'column_header.profile', defaultMessage: 'Profile' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAccountGallery = createSelector(
|
||||||
|
[
|
||||||
|
(state: RootState, accountId: string) =>
|
||||||
|
(state.timelines as ImmutableMap<string, unknown>).getIn(
|
||||||
|
[`account:${accountId}:media`, 'items'],
|
||||||
|
ImmutableList(),
|
||||||
|
) as ImmutableList<string>,
|
||||||
|
(state: RootState) => state.statuses,
|
||||||
|
],
|
||||||
|
(statusIds, statuses) => {
|
||||||
|
let items = ImmutableList<MediaAttachment>();
|
||||||
|
|
||||||
|
statusIds.forEach((statusId) => {
|
||||||
|
const status = statuses.get(statusId) as
|
||||||
|
| ImmutableMap<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
items = items.concat(
|
||||||
|
(
|
||||||
|
status.get('media_attachments') as ImmutableList<MediaAttachment>
|
||||||
|
).map((media) => media.set('status', status)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
acct?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RemoteHint: React.FC<{
|
||||||
|
accountId: string;
|
||||||
|
}> = ({ accountId }) => {
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
const acct = account?.acct;
|
||||||
|
const url = account?.url;
|
||||||
|
const domain = acct ? acct.split('@')[1] : undefined;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimelineHint
|
||||||
|
url={url}
|
||||||
|
message={
|
||||||
|
<FormattedMessage
|
||||||
|
id='hints.profiles.posts_may_be_missing'
|
||||||
|
defaultMessage='Some posts from this profile may be missing.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='hints.profiles.see_more_posts'
|
||||||
|
defaultMessage='See more posts on {domain}'
|
||||||
|
values={{ domain: <strong>{domain}</strong> }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AccountGallery: React.FC<{
|
||||||
|
multiColumn: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { acct, id } = useParams<Params>();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const accountId = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
id ??
|
||||||
|
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
|
||||||
|
);
|
||||||
|
const attachments = useAppSelector((state) =>
|
||||||
|
accountId
|
||||||
|
? getAccountGallery(state, accountId)
|
||||||
|
: ImmutableList<MediaAttachment>(),
|
||||||
|
);
|
||||||
|
const isLoading = useAppSelector((state) =>
|
||||||
|
(state.timelines as ImmutableMap<string, unknown>).getIn([
|
||||||
|
`account:${accountId}:media`,
|
||||||
|
'isLoading',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const hasMore = useAppSelector((state) =>
|
||||||
|
(state.timelines as ImmutableMap<string, unknown>).getIn([
|
||||||
|
`account:${accountId}:media`,
|
||||||
|
'hasMore',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
|
);
|
||||||
|
const blockedBy = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.relationships.getIn([accountId, 'blocked_by'], false) as boolean,
|
||||||
|
);
|
||||||
|
const suspended = useAppSelector(
|
||||||
|
(state) => state.accounts.getIn([accountId, 'suspended'], false) as boolean,
|
||||||
|
);
|
||||||
|
const isAccount = !!account;
|
||||||
|
const remote = account?.acct !== account?.username;
|
||||||
|
const hidden = useAppSelector((state) =>
|
||||||
|
accountId ? getAccountHidden(state, accountId) : false,
|
||||||
|
);
|
||||||
|
const maxId = attachments.last()?.getIn(['status', 'id']) as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!accountId) {
|
||||||
|
dispatch(lookupAccount(acct));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId, acct]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId && !isAccount) {
|
||||||
|
dispatch(fetchAccount(accountId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountId && isAccount) {
|
||||||
|
void dispatch(expandAccountMediaTimeline(accountId));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId, isAccount]);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(() => {
|
||||||
|
if (maxId) {
|
||||||
|
void dispatch(expandAccountMediaTimeline(accountId, { maxId }));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId, maxId]);
|
||||||
|
|
||||||
|
const handleOpenMedia = useCallback(
|
||||||
|
(attachment: MediaAttachment) => {
|
||||||
|
const statusId = attachment.getIn(['status', 'id']);
|
||||||
|
const lang = attachment.getIn(['status', 'language']);
|
||||||
|
|
||||||
|
if (attachment.get('type') === 'video') {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'VIDEO',
|
||||||
|
modalProps: {
|
||||||
|
media: attachment,
|
||||||
|
statusId,
|
||||||
|
lang,
|
||||||
|
options: { autoPlay: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (attachment.get('type') === 'audio') {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'AUDIO',
|
||||||
|
modalProps: {
|
||||||
|
media: attachment,
|
||||||
|
statusId,
|
||||||
|
lang,
|
||||||
|
options: { autoPlay: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const media = attachment.getIn([
|
||||||
|
'status',
|
||||||
|
'media_attachments',
|
||||||
|
]) as ImmutableList<MediaAttachment>;
|
||||||
|
const index = media.findIndex(
|
||||||
|
(x) => x.get('id') === attachment.get('id'),
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'MEDIA',
|
||||||
|
modalProps: { media, index, statusId, lang },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accountId && !isAccount) {
|
||||||
|
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyMessage;
|
||||||
|
|
||||||
|
if (accountId) {
|
||||||
|
if (suspended) {
|
||||||
|
emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_suspended'
|
||||||
|
defaultMessage='Account suspended'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (hidden) {
|
||||||
|
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||||
|
} else if (blockedBy) {
|
||||||
|
emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_unavailable'
|
||||||
|
defaultMessage='Profile unavailable'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (remote && attachments.isEmpty()) {
|
||||||
|
emptyMessage = <RemoteHint accountId={accountId} />;
|
||||||
|
} else {
|
||||||
|
emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_timeline'
|
||||||
|
defaultMessage='No posts found'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const forceEmptyState = suspended || blockedBy || hidden;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
icon='user-circle'
|
||||||
|
iconComponent={PersonIcon}
|
||||||
|
heading={intl.formatMessage(messages.profile)}
|
||||||
|
alwaysShowBackButton
|
||||||
|
>
|
||||||
|
<ScrollableList
|
||||||
|
className='account-gallery__container'
|
||||||
|
prepend={
|
||||||
|
accountId && (
|
||||||
|
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
alwaysPrepend
|
||||||
|
append={remote && accountId && <RemoteHint accountId={accountId} />}
|
||||||
|
scrollKey='account_gallery'
|
||||||
|
isLoading={isLoading}
|
||||||
|
hasMore={!forceEmptyState && hasMore}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
>
|
||||||
|
{attachments.map((attachment) => (
|
||||||
|
<MediaItem
|
||||||
|
key={attachment.get('id') as string}
|
||||||
|
attachment={attachment}
|
||||||
|
onOpenMedia={handleOpenMedia}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default AccountGallery;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,158 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import ActionBar from '../../account/components/action_bar';
|
|
||||||
import InnerHeader from '../../account/components/header';
|
|
||||||
|
|
||||||
import MemorialNote from './memorial_note';
|
|
||||||
import MovedNote from './moved_note';
|
|
||||||
|
|
||||||
class Header extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
account: ImmutablePropTypes.record,
|
|
||||||
onFollow: PropTypes.func.isRequired,
|
|
||||||
onBlock: PropTypes.func.isRequired,
|
|
||||||
onMention: PropTypes.func.isRequired,
|
|
||||||
onDirect: PropTypes.func.isRequired,
|
|
||||||
onReblogToggle: PropTypes.func.isRequired,
|
|
||||||
onReport: PropTypes.func.isRequired,
|
|
||||||
onMute: PropTypes.func.isRequired,
|
|
||||||
onBlockDomain: PropTypes.func.isRequired,
|
|
||||||
onUnblockDomain: PropTypes.func.isRequired,
|
|
||||||
onEndorseToggle: PropTypes.func.isRequired,
|
|
||||||
onAddToList: PropTypes.func.isRequired,
|
|
||||||
onChangeLanguages: PropTypes.func.isRequired,
|
|
||||||
onInteractionModal: PropTypes.func.isRequired,
|
|
||||||
onOpenAvatar: PropTypes.func.isRequired,
|
|
||||||
hideTabs: PropTypes.bool,
|
|
||||||
domain: PropTypes.string.isRequired,
|
|
||||||
hidden: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFollow = () => {
|
|
||||||
this.props.onFollow(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleBlock = () => {
|
|
||||||
this.props.onBlock(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMention = () => {
|
|
||||||
this.props.onMention(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDirect = () => {
|
|
||||||
this.props.onDirect(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleReport = () => {
|
|
||||||
this.props.onReport(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleReblogToggle = () => {
|
|
||||||
this.props.onReblogToggle(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNotifyToggle = () => {
|
|
||||||
this.props.onNotifyToggle(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMute = () => {
|
|
||||||
this.props.onMute(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleBlockDomain = () => {
|
|
||||||
this.props.onBlockDomain(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleUnblockDomain = () => {
|
|
||||||
const domain = this.props.account.get('acct').split('@')[1];
|
|
||||||
|
|
||||||
if (!domain) return;
|
|
||||||
|
|
||||||
this.props.onUnblockDomain(domain);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleEndorseToggle = () => {
|
|
||||||
this.props.onEndorseToggle(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAddToList = () => {
|
|
||||||
this.props.onAddToList(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleEditAccountNote = () => {
|
|
||||||
this.props.onEditAccountNote(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChangeLanguages = () => {
|
|
||||||
this.props.onChangeLanguages(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleInteractionModal = () => {
|
|
||||||
this.props.onInteractionModal(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOpenAvatar = () => {
|
|
||||||
this.props.onOpenAvatar(this.props.account);
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account, hidden, hideTabs } = this.props;
|
|
||||||
|
|
||||||
if (account === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='account-timeline__header'>
|
|
||||||
{(!hidden && account.get('memorial')) && <MemorialNote />}
|
|
||||||
{(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
|
|
||||||
|
|
||||||
<InnerHeader
|
|
||||||
account={account}
|
|
||||||
onFollow={this.handleFollow}
|
|
||||||
onBlock={this.handleBlock}
|
|
||||||
onMention={this.handleMention}
|
|
||||||
onDirect={this.handleDirect}
|
|
||||||
onReblogToggle={this.handleReblogToggle}
|
|
||||||
onNotifyToggle={this.handleNotifyToggle}
|
|
||||||
onReport={this.handleReport}
|
|
||||||
onMute={this.handleMute}
|
|
||||||
onBlockDomain={this.handleBlockDomain}
|
|
||||||
onUnblockDomain={this.handleUnblockDomain}
|
|
||||||
onEndorseToggle={this.handleEndorseToggle}
|
|
||||||
onAddToList={this.handleAddToList}
|
|
||||||
onEditAccountNote={this.handleEditAccountNote}
|
|
||||||
onChangeLanguages={this.handleChangeLanguages}
|
|
||||||
onInteractionModal={this.handleInteractionModal}
|
|
||||||
onOpenAvatar={this.handleOpenAvatar}
|
|
||||||
domain={this.props.domain}
|
|
||||||
hidden={hidden}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ActionBar
|
|
||||||
account={account}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!(hideTabs || hidden) && (
|
|
||||||
<div className='account__section-headline'>
|
|
||||||
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
|
|
||||||
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
|
|
||||||
<NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const MemorialNote = () => (
|
export const MemorialNote: React.FC = () => (
|
||||||
<div className='account-memorial-banner'>
|
<div className='account-memorial-banner'>
|
||||||
<div className='account-memorial-banner__message'>
|
<div className='account-memorial-banner__message'>
|
||||||
<FormattedMessage id='account.in_memoriam' defaultMessage='In Memoriam.' />
|
<FormattedMessage
|
||||||
|
id='account.in_memoriam'
|
||||||
|
defaultMessage='In Memoriam.'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default MemorialNote;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import { AvatarOverlay } from '../../../components/avatar_overlay';
|
|
||||||
import { DisplayName } from '../../../components/display_name';
|
|
||||||
import { Permalink } from '../../../components/permalink';
|
|
||||||
|
|
||||||
export default class MovedNote extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
from: ImmutablePropTypes.map.isRequired,
|
|
||||||
to: ImmutablePropTypes.map.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { from, to } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='moved-account-banner'>
|
|
||||||
<div className='moved-account-banner__message'>
|
|
||||||
<FormattedMessage id='account.moved_to' defaultMessage='{name} has indicated that their new account is now:' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: from.get('display_name_html') }} /></bdi> }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='moved-account-banner__action'>
|
|
||||||
<Permalink to={`/@${to.get('acct')}`} href={to.get('url')} className='detailed-status__display-name'>
|
|
||||||
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
|
|
||||||
<DisplayName account={to} />
|
|
||||||
</Permalink>
|
|
||||||
|
|
||||||
<Permalink to={`/@${to.get('acct')}`} href={to.get('url')} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Permalink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { AvatarOverlay } from 'flavours/glitch/components/avatar_overlay';
|
||||||
|
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||||
|
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
export const MovedNote: React.FC<{
|
||||||
|
accountId: string;
|
||||||
|
targetAccountId: string;
|
||||||
|
}> = ({ accountId, targetAccountId }) => {
|
||||||
|
const from = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
const to = useAppSelector((state) => state.accounts.get(targetAccountId));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='moved-account-banner'>
|
||||||
|
<div className='moved-account-banner__message'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.moved_to'
|
||||||
|
defaultMessage='{name} has indicated that their new account is now:'
|
||||||
|
values={{
|
||||||
|
name: (
|
||||||
|
<bdi>
|
||||||
|
<strong
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: from?.display_name_html ?? '',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</bdi>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='moved-account-banner__action'>
|
||||||
|
<Permalink
|
||||||
|
to={`/@${to?.acct}`}
|
||||||
|
href={to?.url}
|
||||||
|
className='detailed-status__display-name'
|
||||||
|
>
|
||||||
|
<div className='detailed-status__display-avatar'>
|
||||||
|
<AvatarOverlay account={to} friend={from} />
|
||||||
|
</div>
|
||||||
|
<DisplayName account={to} />
|
||||||
|
</Permalink>
|
||||||
|
|
||||||
|
<Permalink to={`/@${to?.acct}`} href={to?.url} className='button'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.go_to_profile'
|
||||||
|
defaultMessage='Go to profile'
|
||||||
|
/>
|
||||||
|
</Permalink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
import { injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import {
|
|
||||||
followAccount,
|
|
||||||
unblockAccount,
|
|
||||||
unmuteAccount,
|
|
||||||
pinAccount,
|
|
||||||
unpinAccount,
|
|
||||||
} from '../../../actions/accounts';
|
|
||||||
import { initBlockModal } from '../../../actions/blocks';
|
|
||||||
import {
|
|
||||||
mentionCompose,
|
|
||||||
directCompose,
|
|
||||||
} from '../../../actions/compose';
|
|
||||||
import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
|
|
||||||
import { openModal } from '../../../actions/modal';
|
|
||||||
import { initMuteModal } from '../../../actions/mutes';
|
|
||||||
import { initReport } from '../../../actions/reports';
|
|
||||||
import { makeGetAccount, getAccountHidden } from '../../../selectors';
|
|
||||||
import Header from '../components/header';
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { accountId }) => ({
|
|
||||||
account: getAccount(state, accountId),
|
|
||||||
domain: state.getIn(['meta', 'domain']),
|
|
||||||
hidden: getAccountHidden(state, accountId),
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
|
|
||||||
onFollow (account) {
|
|
||||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
|
|
||||||
} else {
|
|
||||||
dispatch(followAccount(account.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onInteractionModal (account) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'INTERACTION',
|
|
||||||
modalProps: {
|
|
||||||
type: 'follow',
|
|
||||||
accountId: account.get('id'),
|
|
||||||
url: account.get('uri'),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlock (account) {
|
|
||||||
if (account.getIn(['relationship', 'blocking'])) {
|
|
||||||
dispatch(unblockAccount(account.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(initBlockModal(account));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onMention (account) {
|
|
||||||
dispatch(mentionCompose(account));
|
|
||||||
},
|
|
||||||
|
|
||||||
onDirect (account) {
|
|
||||||
dispatch(directCompose(account));
|
|
||||||
},
|
|
||||||
|
|
||||||
onReblogToggle (account) {
|
|
||||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
|
||||||
dispatch(followAccount(account.get('id'), { reblogs: false }));
|
|
||||||
} else {
|
|
||||||
dispatch(followAccount(account.get('id'), { reblogs: true }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onEndorseToggle (account) {
|
|
||||||
if (account.getIn(['relationship', 'endorsed'])) {
|
|
||||||
dispatch(unpinAccount(account.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(pinAccount(account.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onNotifyToggle (account) {
|
|
||||||
if (account.getIn(['relationship', 'notifying'])) {
|
|
||||||
dispatch(followAccount(account.get('id'), { notify: false }));
|
|
||||||
} else {
|
|
||||||
dispatch(followAccount(account.get('id'), { notify: true }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onReport (account) {
|
|
||||||
dispatch(initReport(account));
|
|
||||||
},
|
|
||||||
|
|
||||||
onMute (account) {
|
|
||||||
if (account.getIn(['relationship', 'muting'])) {
|
|
||||||
dispatch(unmuteAccount(account.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(initMuteModal(account));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlockDomain (account) {
|
|
||||||
dispatch(initDomainBlockModal(account));
|
|
||||||
},
|
|
||||||
|
|
||||||
onUnblockDomain (domain) {
|
|
||||||
dispatch(unblockDomain(domain));
|
|
||||||
},
|
|
||||||
|
|
||||||
onAddToList (account) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'LIST_ADDER',
|
|
||||||
modalProps: {
|
|
||||||
accountId: account.get('id'),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeLanguages (account) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'SUBSCRIBED_LANGUAGES',
|
|
||||||
modalProps: {
|
|
||||||
accountId: account.get('id'),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onOpenAvatar (account) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'IMAGE',
|
|
||||||
modalProps: {
|
|
||||||
src: account.get('avatar'),
|
|
||||||
alt: account.get('acct'),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
|
||||||
import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
|
import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
|
||||||
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
||||||
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
||||||
import { getAccountHidden } from 'flavours/glitch/selectors';
|
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
|
||||||
import { useAppSelector } from 'flavours/glitch/store';
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
||||||
|
|
@ -21,8 +21,8 @@ import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
|
import { AccountHeader } from './components/account_header';
|
||||||
import { LimitedAccountHint } from './components/limited_account_hint';
|
import { LimitedAccountHint } from './components/limited_account_hint';
|
||||||
import HeaderContainer from './containers/header_container';
|
|
||||||
|
|
||||||
const emptyList = ImmutableList();
|
const emptyList = ImmutableList();
|
||||||
|
|
||||||
|
|
@ -199,7 +199,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
|
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import { useSelectableClick } from '@/hooks/useSelectableClick';
|
|
||||||
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
|
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { useSelectableClick } from 'flavours/glitch/hooks/useSelectableClick';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
help: { id: 'info_button.label', defaultMessage: 'Help' },
|
help: { id: 'info_button.label', defaultMessage: 'Help' },
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
|
@ -28,28 +28,30 @@ export const ActionBar = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleLogoutClick = useCallback(() => {
|
const menu = useMemo(() => {
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
|
const handleLogoutClick = () => {
|
||||||
}, [dispatch]);
|
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
|
||||||
|
};
|
||||||
|
|
||||||
let menu = [];
|
return ([
|
||||||
|
{ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' },
|
||||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
{ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' },
|
||||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
{ text: intl.formatMessage(messages.pins), to: '/pinned' },
|
||||||
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
null,
|
||||||
menu.push(null);
|
{ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' },
|
||||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
{ text: intl.formatMessage(messages.favourites), to: '/favourites' },
|
||||||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
{ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' },
|
||||||
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
|
{ text: intl.formatMessage(messages.lists), to: '/lists' },
|
||||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
{ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' },
|
||||||
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
null,
|
||||||
menu.push(null);
|
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
|
||||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
|
||||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
{ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' },
|
||||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
{ text: intl.formatMessage(messages.filters), href: '/filters' },
|
||||||
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
|
null,
|
||||||
menu.push(null);
|
{ text: intl.formatMessage(messages.logout), action: handleLogoutClick },
|
||||||
menu.push({ text: intl.formatMessage(messages.logout), action: handleLogoutClick });
|
]);
|
||||||
|
}, [intl, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuContainer
|
<DropdownMenuContainer
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onSubmit(missingAltTextModal && this.props.missingAltText, overridePrivacy);
|
this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct', overridePrivacy);
|
||||||
|
|
||||||
if (e) {
|
if (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,15 @@ import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
|
import emojiCompressed from 'flavours/glitch/features/emoji/emoji_compressed';
|
||||||
import { useSystemEmojiFont } from 'flavours/glitch/initial_state';
|
import { useSystemEmojiFont } from 'flavours/glitch/initial_state';
|
||||||
import { assetHost } from 'flavours/glitch/utils/config';
|
import { assetHost } from 'flavours/glitch/utils/config';
|
||||||
|
|
||||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||||
|
|
||||||
|
const nimblePickerData = emojiCompressed[5];
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
||||||
|
|
@ -38,15 +41,18 @@ let EmojiPicker, Emoji; // load asynchronously
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||||
|
|
||||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
|
const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
|
||||||
|
|
||||||
const notFoundFn = () => (
|
const notFoundFn = () => (
|
||||||
<div className='emoji-mart-no-results'>
|
<div className='emoji-mart-no-results'>
|
||||||
<Emoji
|
<Emoji
|
||||||
|
data={nimblePickerData}
|
||||||
emoji='sleuth_or_spy'
|
emoji='sleuth_or_spy'
|
||||||
set='twitter'
|
set='twitter'
|
||||||
size={32}
|
size={32}
|
||||||
sheetSize={32}
|
sheetSize={32}
|
||||||
|
sheetColumns={62}
|
||||||
|
sheetRows={62}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -68,7 +74,7 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
|
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.active) {
|
if (nextProps.active) {
|
||||||
this.attachListeners();
|
this.attachListeners();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -76,7 +82,7 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
this.removeListeners();
|
this.removeListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,12 +92,12 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
attachListeners () {
|
attachListeners() {
|
||||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListeners () {
|
removeListeners() {
|
||||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
}
|
}
|
||||||
|
|
@ -100,17 +106,17 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { active } = this.props;
|
const { active } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
||||||
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={1}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={2}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={3}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={4}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={5}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={6}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +151,7 @@ class ModifierPicker extends PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji-picker-dropdown__modifiers'>
|
<div className='emoji-picker-dropdown__modifiers'>
|
||||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
|
<Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
|
||||||
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -185,7 +191,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount() {
|
||||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
|
||||||
|
|
@ -200,7 +206,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +259,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
this.props.onSkinTone(modifier);
|
this.props.onSkinTone(modifier);
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
|
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -281,6 +287,9 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
|
data={nimblePickerData}
|
||||||
|
sheetColumns={62}
|
||||||
|
sheetRows={62}
|
||||||
perLine={8}
|
perLine={8}
|
||||||
emojiSize={22}
|
emojiSize={22}
|
||||||
sheetSize={32}
|
sheetSize={32}
|
||||||
|
|
@ -347,7 +356,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
|
|
||||||
EmojiPickerAsync().then(EmojiMart => {
|
EmojiPickerAsync().then(EmojiMart => {
|
||||||
EmojiPicker = EmojiMart.Picker;
|
EmojiPicker = EmojiMart.Picker;
|
||||||
Emoji = EmojiMart.Emoji;
|
Emoji = EmojiMart.Emoji;
|
||||||
|
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|
@ -388,7 +397,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
this.setState({ placement: state.placement });
|
this.setState({ placement: state.placement });
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
||||||
const title = intl.formatMessage(messages.emoji);
|
const title = intl.formatMessage(messages.emoji);
|
||||||
const { active, loading, placement } = this.state;
|
const { active, loading, placement } = this.state;
|
||||||
|
|
@ -405,7 +414,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
<Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||||
{({ props, placement })=> (
|
{({ props, placement }) => (
|
||||||
<div {...props} style={{ ...props.style }}>
|
<div {...props} style={{ ...props.style }}>
|
||||||
<div className={`dropdown-animation ${placement}`}>
|
<div className={`dropdown-animation ${placement}`}>
|
||||||
<EmojiPickerMenu
|
<EmojiPickerMenu
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,9 @@ import { LoadMore } from 'flavours/glitch/components/load_more';
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
import { RadioButton } from 'flavours/glitch/components/radio_button';
|
import { RadioButton } from 'flavours/glitch/components/radio_button';
|
||||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||||
|
import { useSearchParam } from 'flavours/glitch/hooks/useSearchParam';
|
||||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import { useSearchParam } from '../../hooks/useSearchParam';
|
|
||||||
|
|
||||||
import { AccountCard } from './components/account_card';
|
import { AccountCard } from './components/account_card';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ type EmojiCompressed = [
|
||||||
Category[],
|
Category[],
|
||||||
Data['aliases'],
|
Data['aliases'],
|
||||||
EmojisWithoutShortCodes,
|
EmojisWithoutShortCodes,
|
||||||
|
Data,
|
||||||
];
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,91 @@
|
||||||
|
|
||||||
// This version comment should be bumped each time the emoji data is changed
|
// This version comment should be bumped each time the emoji data is changed
|
||||||
// to ensure that the prevaled file is regenerated by Babel
|
// to ensure that the prevaled file is regenerated by Babel
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
const { emojiIndex } = require('emoji-mart');
|
// This json file contains the names of the categories.
|
||||||
let data = require('emoji-mart/data/all.json');
|
const emojiMart5LocalesData = require('@emoji-mart/data/i18n/en.json');
|
||||||
|
const emojiMart5Data = require('@emoji-mart/data/sets/15/all.json');
|
||||||
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
|
||||||
const emojiMap = require('./emoji_map.json');
|
const emojiMap = require('./emoji_map.json');
|
||||||
|
// This json file is downloaded from https://github.com/iamcal/emoji-data/
|
||||||
|
// and is used to correct the sheet coordinates since we're using that repo's sheet
|
||||||
|
const emojiSheetData = require('./emoji_sheet.json');
|
||||||
const { unicodeToFilename } = require('./unicode_to_filename');
|
const { unicodeToFilename } = require('./unicode_to_filename');
|
||||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||||
|
|
||||||
if(data.compressed) {
|
// Grabbed from `emoji_utils` to avoid circular dependency
|
||||||
data = emojiMartUncompress(data);
|
function unifiedToNative(unified) {
|
||||||
|
let unicodes = unified.split('-'),
|
||||||
|
codePoints = unicodes.map((u) => `0x${u}`);
|
||||||
|
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
compressed: true,
|
||||||
|
categories: emojiMart5Data.categories.map(cat => {
|
||||||
|
return {
|
||||||
|
...cat,
|
||||||
|
name: emojiMart5LocalesData.categories[cat.id]
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
aliases: emojiMart5Data.aliases,
|
||||||
|
emojis: _(emojiMart5Data.emojis).values().map(emoji => {
|
||||||
|
let skin_variations = {};
|
||||||
|
const unified = emoji.skins[0].unified.toUpperCase();
|
||||||
|
const emojiFromRawData = emojiSheetData.find(e => e.unified === unified);
|
||||||
|
|
||||||
|
if (!emojiFromRawData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji.skins.length > 1) {
|
||||||
|
const [, ...nonDefaultSkins] = emoji.skins;
|
||||||
|
nonDefaultSkins.forEach(skin => {
|
||||||
|
const [matchingRawCodePoints,matchingRawEmoji] = Object.entries(emojiFromRawData.skin_variations).find((pair) => {
|
||||||
|
const [, value] = pair;
|
||||||
|
return value.unified.toLowerCase() === skin.unified;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingRawEmoji && matchingRawCodePoints) {
|
||||||
|
// At the time of writing, the json from `@emoji-mart/data` doesn't have data
|
||||||
|
// for emoji like `woman-heart-woman` with two different skin tones.
|
||||||
|
const skinToneCode = matchingRawCodePoints.split('-')[0];
|
||||||
|
skin_variations[skinToneCode] = {
|
||||||
|
unified: matchingRawEmoji.unified.toUpperCase(),
|
||||||
|
non_qualified: null,
|
||||||
|
sheet_x: matchingRawEmoji.sheet_x,
|
||||||
|
sheet_y: matchingRawEmoji.sheet_y,
|
||||||
|
has_img_twitter: true,
|
||||||
|
native: unifiedToNative(matchingRawEmoji.unified.toUpperCase())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
a: emoji.name,
|
||||||
|
b: unified,
|
||||||
|
c: undefined,
|
||||||
|
f: true,
|
||||||
|
j: [emoji.id, ...emoji.keywords],
|
||||||
|
k: [emojiFromRawData.sheet_x, emojiFromRawData.sheet_y],
|
||||||
|
m: emoji.emoticons?.[0],
|
||||||
|
l: emoji.emoticons,
|
||||||
|
o: emoji.version,
|
||||||
|
id: emoji.id,
|
||||||
|
skin_variations,
|
||||||
|
native: unifiedToNative(unified.toUpperCase())
|
||||||
|
};
|
||||||
|
}).compact().keyBy(e => e.id).mapValues(e => _.omit(e, 'id')).value()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.compressed) {
|
||||||
|
emojiMartUncompress(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojiMartData = data;
|
const emojiMartData = data;
|
||||||
|
|
@ -32,15 +105,10 @@ const shortcodeMap = {};
|
||||||
const shortCodesToEmojiData = {};
|
const shortCodesToEmojiData = {};
|
||||||
const emojisWithoutShortCodes = [];
|
const emojisWithoutShortCodes = [];
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
Object.keys(emojiMart5Data.emojis).forEach(key => {
|
||||||
let emoji = emojiIndex.emojis[key];
|
let emoji = emojiMart5Data.emojis[key];
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
shortcodeMap[emoji.skins[0].native] = emoji.id;
|
||||||
if (Object.hasOwn(emoji, '1')) {
|
|
||||||
emoji = emoji['1'];
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcodeMap[emoji.native] = emoji.id;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const stripModifiers = unicode => {
|
const stripModifiers = unicode => {
|
||||||
|
|
@ -84,13 +152,9 @@ Object.keys(emojiMap).forEach(key => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
Object.keys(emojiMartData.emojis).forEach(key => {
|
||||||
let emoji = emojiIndex.emojis[key];
|
let emoji = emojiMartData.emojis[key];
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
|
||||||
if (Object.hasOwn(emoji, '1')) {
|
|
||||||
emoji = emoji['1'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { native } = emoji;
|
const { native } = emoji;
|
||||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||||
|
|
@ -135,4 +199,5 @@ module.exports = JSON.parse(JSON.stringify([
|
||||||
emojiMartData.categories,
|
emojiMartData.categories,
|
||||||
emojiMartData.aliases,
|
emojiMartData.aliases,
|
||||||
emojisWithoutShortCodes,
|
emojisWithoutShortCodes,
|
||||||
|
emojiMartData
|
||||||
]));
|
]));
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,15 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
|
||||||
import emojiCompressed from './emoji_compressed';
|
import emojiCompressed from './emoji_compressed';
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||||
|
|
||||||
type Emojis = {
|
type Emojis = Record<
|
||||||
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
|
NonNullable<keyof ShortCodesToEmojiData>,
|
||||||
|
{
|
||||||
native: BaseEmoji['native'];
|
native: BaseEmoji['native'];
|
||||||
search: Search;
|
search: Search;
|
||||||
short_names: Emoji['short_names'];
|
short_names: Emoji['short_names'];
|
||||||
unified: Emoji['unified'];
|
unified: Emoji['unified'];
|
||||||
};
|
}
|
||||||
};
|
>;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
|
import Emoji from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
||||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
import Picker from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Picker,
|
Picker,
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -9,12 +9,13 @@ import type {
|
||||||
import emojiCompressed from './emoji_compressed';
|
import emojiCompressed from './emoji_compressed';
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename } from './unicode_to_filename';
|
||||||
|
|
||||||
type UnicodeMapping = {
|
type UnicodeMapping = Record<
|
||||||
[key in FilenameData[number][0]]: {
|
FilenameData[number][0],
|
||||||
|
{
|
||||||
shortCode: ShortCodesToEmojiDataKey;
|
shortCode: ShortCodesToEmojiDataKey;
|
||||||
filename: FilenameData[number][number];
|
filename: FilenameData[number][number];
|
||||||
};
|
}
|
||||||
};
|
>;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { Account } from 'flavours/glitch/components/account';
|
import { Account } from 'flavours/glitch/components/account';
|
||||||
import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
|
import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
|
||||||
|
import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header';
|
||||||
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
||||||
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
||||||
import { getAccountHidden } from 'flavours/glitch/selectors';
|
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
|
||||||
import { useAppSelector } from 'flavours/glitch/store';
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,7 +26,6 @@ import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import ProfileColumnHeader from '../account/components/profile_column_header';
|
import ProfileColumnHeader from '../account/components/profile_column_header';
|
||||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
|
|
@ -172,7 +172,7 @@ class Followers extends ImmutablePureComponent {
|
||||||
hasMore={!forceEmptyState && hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { Account } from 'flavours/glitch/components/account';
|
import { Account } from 'flavours/glitch/components/account';
|
||||||
import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
|
import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
|
||||||
|
import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header';
|
||||||
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
||||||
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
||||||
import { getAccountHidden } from 'flavours/glitch/selectors';
|
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
|
||||||
import { useAppSelector } from 'flavours/glitch/store';
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,7 +26,6 @@ import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import ProfileColumnHeader from '../account/components/profile_column_header';
|
import ProfileColumnHeader from '../account/components/profile_column_header';
|
||||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
|
|
@ -172,7 +172,7 @@ class Following extends ImmutablePureComponent {
|
||||||
hasMore={!forceEmptyState && hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export const ColumnSettings: React.FC = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(key: string, checked: boolean) => {
|
(key: string[], checked: boolean) => {
|
||||||
dispatch(changeSetting(['home', ...key], checked));
|
dispatch(changeSetting(['home', ...key], checked));
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { useSearchParam } from '@/hooks/useSearchParam';
|
|
||||||
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||||
|
|
@ -20,6 +19,7 @@ import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
import Status from 'flavours/glitch/containers/status_container';
|
import Status from 'flavours/glitch/containers/status_container';
|
||||||
import { Search } from 'flavours/glitch/features/compose/components/search';
|
import { Search } from 'flavours/glitch/features/compose/components/search';
|
||||||
|
import { useSearchParam } from 'flavours/glitch/hooks/useSearchParam';
|
||||||
import type { Hashtag as HashtagType } from 'flavours/glitch/models/tags';
|
import type { Hashtag as HashtagType } from 'flavours/glitch/models/tags';
|
||||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -243,12 +243,12 @@ export const DetailedStatus: React.FC<{
|
||||||
);
|
);
|
||||||
mediaIcons.push('video-camera');
|
mediaIcons.push('video-camera');
|
||||||
}
|
}
|
||||||
} else if (status.get('spoiler_text').length === 0) {
|
} else if (status.get('card')) {
|
||||||
media = (
|
media = (
|
||||||
<Card
|
<Card
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
onOpenMedia={onOpenMedia}
|
onOpenMedia={onOpenMedia}
|
||||||
card={status.get('card', null)}
|
card={status.get('card')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
mediaIcons.push('link');
|
mediaIcons.push('link');
|
||||||
|
|
|
||||||
|
|
@ -8,26 +8,31 @@ import {
|
||||||
} from 'react-intl';
|
} from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { apiGetTermsOfService } from 'flavours/glitch/api/instance';
|
import { apiGetTermsOfService } from 'flavours/glitch/api/instance';
|
||||||
import type { ApiTermsOfServiceJSON } from 'flavours/glitch/api_types/instance';
|
import type { ApiTermsOfServiceJSON } from 'flavours/glitch/api_types/instance';
|
||||||
import { Column } from 'flavours/glitch/components/column';
|
import { Column } from 'flavours/glitch/components/column';
|
||||||
import { Skeleton } from 'flavours/glitch/components/skeleton';
|
|
||||||
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
|
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
date?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const TermsOfService: React.FC<{
|
const TermsOfService: React.FC<{
|
||||||
multiColumn: boolean;
|
multiColumn: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { date } = useParams<Params>();
|
||||||
const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
|
const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiGetTermsOfService()
|
apiGetTermsOfService(date)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setResponse(data);
|
setResponse(data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -36,7 +41,7 @@ const TermsOfService: React.FC<{
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [date]);
|
||||||
|
|
||||||
if (!loading && !response) {
|
if (!loading && !response) {
|
||||||
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
||||||
|
|
@ -55,23 +60,60 @@ const TermsOfService: React.FC<{
|
||||||
defaultMessage='Terms of Service'
|
defaultMessage='Terms of Service'
|
||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p className='prose'>
|
||||||
<FormattedMessage
|
{response?.effective ? (
|
||||||
id='privacy_policy.last_updated'
|
<FormattedMessage
|
||||||
defaultMessage='Last updated {date}'
|
id='privacy_policy.last_updated'
|
||||||
values={{
|
defaultMessage='Last updated {date}'
|
||||||
date: loading ? (
|
values={{
|
||||||
<Skeleton width='10ch' />
|
date: (
|
||||||
) : (
|
<FormattedDate
|
||||||
<FormattedDate
|
value={response.effective_date}
|
||||||
value={response?.updated_at}
|
year='numeric'
|
||||||
year='numeric'
|
month='short'
|
||||||
month='short'
|
day='2-digit'
|
||||||
day='2-digit'
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='terms_of_service.effective_as_of'
|
||||||
|
defaultMessage='Effective as of {date}'
|
||||||
|
values={{
|
||||||
|
date: (
|
||||||
|
<FormattedDate
|
||||||
|
value={response?.effective_date}
|
||||||
|
year='numeric'
|
||||||
|
month='short'
|
||||||
|
day='2-digit'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{response?.succeeded_by && (
|
||||||
|
<>
|
||||||
|
{' · '}
|
||||||
|
<Link to={`/terms-of-service/${response.succeeded_by}`}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='terms_of_service.upcoming_changes_on'
|
||||||
|
defaultMessage='Upcoming changes on {date}'
|
||||||
|
values={{
|
||||||
|
date: (
|
||||||
|
<FormattedDate
|
||||||
|
value={response.succeeded_by}
|
||||||
|
year='numeric'
|
||||||
|
month='short'
|
||||||
|
day='2-digit'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
</Link>
|
||||||
}}
|
</>
|
||||||
/>
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ const EmbedModal: React.FC<{
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||||
frameBorder='0'
|
frameBorder='0'
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
sandbox='allow-scripts allow-same-origin'
|
sandbox='allow-scripts allow-same-origin'
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||||
<WrappedRoute path='/about' component={About} content={children} />
|
<WrappedRoute path='/about' component={About} content={children} />
|
||||||
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
||||||
<WrappedRoute path='/terms-of-service' component={TermsOfService} content={children} />
|
<WrappedRoute path='/terms-of-service/:date?' component={TermsOfService} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
||||||
<Redirect from='/timelines/public' to='/public' exact />
|
<Redirect from='/timelines/public' to='/public' exact />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
interface BaseMenuItem {
|
||||||
|
text: string;
|
||||||
|
dangerous?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionMenuItem extends BaseMenuItem {
|
||||||
|
action: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LinkMenuItem extends BaseMenuItem {
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExternalLinkMenuItem extends BaseMenuItem {
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MenuItem =
|
||||||
|
| ActionMenuItem
|
||||||
|
| LinkMenuItem
|
||||||
|
| ExternalLinkMenuItem
|
||||||
|
| null;
|
||||||
|
|
||||||
|
export type DropdownMenu = MenuItem[];
|
||||||
|
|
@ -318,12 +318,26 @@ const expiresInFromExpiresAt = expires_at => {
|
||||||
|
|
||||||
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
||||||
prefix = prefix.toLowerCase();
|
prefix = prefix.toLowerCase();
|
||||||
|
|
||||||
if (suggestions.length < 4) {
|
if (suggestions.length < 4) {
|
||||||
const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
|
const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
|
||||||
return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
suggestions = suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
||||||
} else {
|
|
||||||
return suggestions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefer capitalization from personal history, unless personal history is all lower-case
|
||||||
|
const fixSuggestionCapitalization = (suggestion) => {
|
||||||
|
if (suggestion.type !== 'hashtag')
|
||||||
|
return suggestion;
|
||||||
|
|
||||||
|
const tagFromHistory = tagHistory.find((tag) => tag.localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) === 0);
|
||||||
|
|
||||||
|
if (!tagFromHistory || tagFromHistory.toLowerCase() === tagFromHistory)
|
||||||
|
return suggestion;
|
||||||
|
|
||||||
|
return { ...suggestion, name: tagFromHistory };
|
||||||
|
};
|
||||||
|
|
||||||
|
return suggestions.map(fixSuggestionCapitalization);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
|
const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { Record as ImmutableRecord } from 'immutable';
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
import { accountDefaultValues } from 'flavours/glitch/models/account';
|
import { accountDefaultValues } from 'flavours/glitch/models/account';
|
||||||
import type { Account, AccountShape } from 'flavours/glitch/models/account';
|
import type { Account, AccountShape } from 'flavours/glitch/models/account';
|
||||||
import type { Relationship } from 'flavours/glitch/models/relationship';
|
import type { Relationship } from 'flavours/glitch/models/relationship';
|
||||||
|
|
@ -45,3 +46,16 @@ export function makeGetAccount() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAccountHidden = createSelector(
|
||||||
|
[
|
||||||
|
(state: RootState, id: string) => state.accounts.get(id)?.hidden,
|
||||||
|
(state: RootState, id: string) =>
|
||||||
|
state.relationships.get(id)?.following ||
|
||||||
|
state.relationships.get(id)?.requested,
|
||||||
|
(state: RootState, id: string) => id === me,
|
||||||
|
],
|
||||||
|
(hidden, followingOrRequested, isSelf) => {
|
||||||
|
return hidden && !(isSelf || followingOrRequested);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -92,29 +92,6 @@ export const makeGetReport = () => createSelector([
|
||||||
(state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]),
|
(state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]),
|
||||||
], (base, targetAccount) => base.set('target_account', targetAccount));
|
], (base, targetAccount) => base.set('target_account', targetAccount));
|
||||||
|
|
||||||
export const getAccountGallery = createSelector([
|
|
||||||
(state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
|
|
||||||
state => state.get('statuses'),
|
|
||||||
(state, id) => state.getIn(['accounts', id]),
|
|
||||||
], (statusIds, statuses, account) => {
|
|
||||||
let medias = ImmutableList();
|
|
||||||
|
|
||||||
statusIds.forEach(statusId => {
|
|
||||||
const status = statuses.get(statusId);
|
|
||||||
medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status).set('account', account)));
|
|
||||||
});
|
|
||||||
|
|
||||||
return medias;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getAccountHidden = createSelector([
|
|
||||||
(state, id) => state.getIn(['accounts', id, 'hidden']),
|
|
||||||
(state, id) => state.getIn(['relationships', id, 'following']) || state.getIn(['relationships', id, 'requested']),
|
|
||||||
(state, id) => id === me,
|
|
||||||
], (hidden, followingOrRequested, isSelf) => {
|
|
||||||
return hidden && !(isSelf || followingOrRequested);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getStatusList = createSelector([
|
export const getStatusList = createSelector([
|
||||||
(state, type) => state.getIn(['status_lists', type, 'items']),
|
(state, type) => state.getIn(['status_lists', type, 'items']),
|
||||||
], (items) => items.toList());
|
], (items) => items.toList());
|
||||||
|
|
|
||||||
|
|
@ -2010,6 +2010,11 @@ a.sparkline {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7181,6 +7181,8 @@ img.modal-warning {
|
||||||
}
|
}
|
||||||
|
|
||||||
&--layout-3 {
|
&--layout-3 {
|
||||||
|
min-height: calc(64px * 2 + 8px);
|
||||||
|
|
||||||
& > .media-gallery__item:nth-child(1) {
|
& > .media-gallery__item:nth-child(1) {
|
||||||
border-end-end-radius: 0;
|
border-end-end-radius: 0;
|
||||||
border-start-end-radius: 0;
|
border-start-end-radius: 0;
|
||||||
|
|
@ -7200,6 +7202,8 @@ img.modal-warning {
|
||||||
}
|
}
|
||||||
|
|
||||||
&--layout-4 {
|
&--layout-4 {
|
||||||
|
min-height: calc(64px * 2 + 8px);
|
||||||
|
|
||||||
& > .media-gallery__item:nth-child(1) {
|
& > .media-gallery__item:nth-child(1) {
|
||||||
border-end-end-radius: 0;
|
border-end-end-radius: 0;
|
||||||
border-start-end-radius: 0;
|
border-start-end-radius: 0;
|
||||||
|
|
@ -7805,7 +7809,8 @@ img.modal-warning {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-more {
|
.load-more,
|
||||||
|
.timeline-hint {
|
||||||
grid-column: span 3;
|
grid-column: span 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -341,10 +341,17 @@ code {
|
||||||
columns: unset;
|
columns: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input.datetime .label_input select {
|
.input.datetime .label_input,
|
||||||
display: inline-block;
|
.input.date .label_input {
|
||||||
width: auto;
|
display: flex;
|
||||||
flex: 0;
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
select {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input.select.select--languages {
|
.input.select.select--languages {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => {
|
||||||
api().put('/api/web/settings', { data })
|
api().put('/api/web/settings', { data })
|
||||||
.then(() => dispatch({ type: SETTING_SAVE }))
|
.then(() => dispatch({ type: SETTING_SAVE }))
|
||||||
.catch(error => dispatch(showAlertForError(error)));
|
.catch(error => dispatch(showAlertForError(error)));
|
||||||
}, 5000, { trailing: true });
|
}, 2000, { leading: true, trailing: true });
|
||||||
|
|
||||||
export function saveSettings() {
|
export function saveSettings() {
|
||||||
return (dispatch, getState) => debouncedSave(dispatch, getState);
|
return (dispatch, getState) => debouncedSave(dispatch, getState);
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export function deleteStatus(id, withRedraft = false) {
|
||||||
|
|
||||||
dispatch(deleteStatusRequest(id));
|
dispatch(deleteStatusRequest(id));
|
||||||
|
|
||||||
api().delete(`/api/v1/statuses/${id}`).then(response => {
|
api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
|
||||||
dispatch(deleteStatusSuccess(id));
|
dispatch(deleteStatusSuccess(id));
|
||||||
dispatch(deleteFromTimelines(id));
|
dispatch(deleteFromTimelines(id));
|
||||||
dispatch(importFetchedAccount(response.data.account));
|
dispatch(importFetchedAccount(response.data.account));
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,12 @@ import type {
|
||||||
ApiPrivacyPolicyJSON,
|
ApiPrivacyPolicyJSON,
|
||||||
} from 'mastodon/api_types/instance';
|
} from 'mastodon/api_types/instance';
|
||||||
|
|
||||||
export const apiGetTermsOfService = () =>
|
export const apiGetTermsOfService = (version?: string) =>
|
||||||
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
|
apiRequestGet<ApiTermsOfServiceJSON>(
|
||||||
|
version
|
||||||
|
? `v1/instance/terms_of_service/${version}`
|
||||||
|
: 'v1/instance/terms_of_service',
|
||||||
|
);
|
||||||
|
|
||||||
export const apiGetPrivacyPolicy = () =>
|
export const apiGetPrivacyPolicy = () =>
|
||||||
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
export interface ApiTermsOfServiceJSON {
|
export interface ApiTermsOfServiceJSON {
|
||||||
updated_at: string;
|
effective_date: string;
|
||||||
|
effective: boolean;
|
||||||
|
succeeded_by: string | null;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLinks } from 'mastodon/../hooks/useLinks';
|
import { useLinks } from 'mastodon/hooks/useLinks';
|
||||||
|
|
||||||
export const AccountBio: React.FC<{
|
export const AccountBio: React.FC<{
|
||||||
note: string;
|
note: string;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
import { useLinks } from 'mastodon/../hooks/useLinks';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { useLinks } from 'mastodon/hooks/useLinks';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
export const AccountFields: React.FC<{
|
export const AccountFields: React.FC<{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
UsePopperOptions,
|
UsePopperOptions,
|
||||||
} from 'react-overlays/esm/usePopper';
|
} from 'react-overlays/esm/usePopper';
|
||||||
|
|
||||||
import { useSelectableClick } from '@/hooks/useSelectableClick';
|
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
|
||||||
|
|
||||||
const offset = [0, 4] as OffsetValue;
|
const offset = [0, 4] as OffsetValue;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useHovering } from 'mastodon/../hooks/useHovering';
|
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||||
import { autoPlayGif } from 'mastodon/initial_state';
|
import { autoPlayGif } from 'mastodon/initial_state';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||||
|
import { autoPlayGif } from 'mastodon/initial_state';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
import { useHovering } from '../../hooks/useHovering';
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
|
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
|
||||||
import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { useTimeout } from 'mastodon/hooks/useTimeout';
|
||||||
|
|
||||||
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
|
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useHovering } from '@/hooks/useHovering';
|
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||||
import { autoPlayGif } from 'mastodon/initial_state';
|
import { autoPlayGif } from 'mastodon/initial_state';
|
||||||
|
|
||||||
export const GIF: React.FC<{
|
export const GIF: React.FC<{
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
|
||||||
<Sparklines
|
<Sparklines
|
||||||
width={50}
|
width={50}
|
||||||
height={28}
|
height={28}
|
||||||
data={history ? history : Array.from(Array(7)).map(() => 0)}
|
data={history ?? Array.from(Array(7)).map(() => 0)}
|
||||||
>
|
>
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
</Sparklines>
|
</Sparklines>
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import type {
|
||||||
UsePopperOptions,
|
UsePopperOptions,
|
||||||
} from 'react-overlays/esm/usePopper';
|
} from 'react-overlays/esm/usePopper';
|
||||||
|
|
||||||
import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
|
||||||
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
||||||
|
import { useTimeout } from 'mastodon/hooks/useTimeout';
|
||||||
|
|
||||||
const offset = [-12, 4] as OffsetValue;
|
const offset = [-12, 4] as OffsetValue;
|
||||||
const enterDelay = 750;
|
const enterDelay = 750;
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ export class IconButton extends PureComponent<Props, States> {
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ class ScrollableList extends PureComponent {
|
||||||
bindToDocument: PropTypes.bool,
|
bindToDocument: PropTypes.bool,
|
||||||
preventScroll: PropTypes.bool,
|
preventScroll: PropTypes.bool,
|
||||||
footer: PropTypes.node,
|
footer: PropTypes.node,
|
||||||
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
|
@ -325,7 +326,7 @@ class ScrollableList extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
const { children, scrollKey, className, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
||||||
const { fullscreen } = this.state;
|
const { fullscreen } = this.state;
|
||||||
const childrenCount = Children.count(children);
|
const childrenCount = Children.count(children);
|
||||||
|
|
||||||
|
|
@ -336,9 +337,9 @@ class ScrollableList extends PureComponent {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
scrollableArea = (
|
scrollableArea = (
|
||||||
<div className='scrollable scrollable--flex' ref={this.setRef}>
|
<div className='scrollable scrollable--flex' ref={this.setRef}>
|
||||||
<div role='feed' className='item-list'>
|
{prepend}
|
||||||
{prepend}
|
|
||||||
</div>
|
<div role='feed' className='item-list' />
|
||||||
|
|
||||||
<div className='scrollable__append'>
|
<div className='scrollable__append'>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
|
|
@ -350,9 +351,9 @@ class ScrollableList extends PureComponent {
|
||||||
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
|
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
|
||||||
scrollableArea = (
|
scrollableArea = (
|
||||||
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||||
<div role='feed' className='item-list'>
|
{prepend}
|
||||||
{prepend}
|
|
||||||
|
|
||||||
|
<div role='feed' className={classNames('item-list', className)}>
|
||||||
{loadPending}
|
{loadPending}
|
||||||
|
|
||||||
{Children.map(this.props.children, (child, index) => (
|
{Children.map(this.props.children, (child, index) => (
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,15 @@ import { Icon } from 'mastodon/components/icon';
|
||||||
import { formatTime } from 'mastodon/features/video';
|
import { formatTime } from 'mastodon/features/video';
|
||||||
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
|
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
|
||||||
import type { Status, MediaAttachment } from 'mastodon/models/status';
|
import type { Status, MediaAttachment } from 'mastodon/models/status';
|
||||||
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
export const MediaItem: React.FC<{
|
export const MediaItem: React.FC<{
|
||||||
attachment: MediaAttachment;
|
attachment: MediaAttachment;
|
||||||
onOpenMedia: (arg0: MediaAttachment) => void;
|
onOpenMedia: (arg0: MediaAttachment) => void;
|
||||||
}> = ({ attachment, onOpenMedia }) => {
|
}> = ({ attachment, onOpenMedia }) => {
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
state.accounts.get(attachment.getIn(['status', 'account']) as string),
|
||||||
|
);
|
||||||
const [visible, setVisible] = useState(
|
const [visible, setVisible] = useState(
|
||||||
(displayMedia !== 'hide_all' &&
|
(displayMedia !== 'hide_all' &&
|
||||||
!attachment.getIn(['status', 'sensitive'])) ||
|
!attachment.getIn(['status', 'sensitive'])) ||
|
||||||
|
|
@ -66,11 +70,10 @@ export const MediaItem: React.FC<{
|
||||||
attachment.get('description')) as string | undefined;
|
attachment.get('description')) as string | undefined;
|
||||||
const previewUrl = attachment.get('preview_url') as string;
|
const previewUrl = attachment.get('preview_url') as string;
|
||||||
const fullUrl = attachment.get('url') as string;
|
const fullUrl = attachment.get('url') as string;
|
||||||
const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
|
const avatarUrl = account?.avatar_static;
|
||||||
const lang = status.get('language') as string;
|
const lang = status.get('language') as string;
|
||||||
const blurhash = attachment.get('blurhash') as string;
|
const blurhash = attachment.get('blurhash') as string;
|
||||||
const statusId = status.get('id') as string;
|
const statusId = status.get('id') as string;
|
||||||
const acct = status.getIn(['account', 'acct']) as string;
|
|
||||||
const type = attachment.get('type') as string;
|
const type = attachment.get('type') as string;
|
||||||
|
|
||||||
let thumbnail;
|
let thumbnail;
|
||||||
|
|
@ -181,7 +184,7 @@ export const MediaItem: React.FC<{
|
||||||
|
|
||||||
<a
|
<a
|
||||||
className='media-gallery__item-thumbnail'
|
className='media-gallery__item-thumbnail'
|
||||||
href={`/@${acct}/${statusId}`}
|
href={`/@${account?.acct}/${statusId}`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
|
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
|
||||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
|
||||||
import { LoadMore } from 'mastodon/components/load_more';
|
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
|
||||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
|
||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
|
||||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
|
||||||
import { getAccountGallery } from 'mastodon/selectors';
|
|
||||||
|
|
||||||
import { expandAccountMediaTimeline } from '../../actions/timelines';
|
|
||||||
import { AccountHeader } from '../account_timeline/components/account_header';
|
|
||||||
import Column from '../ui/components/column';
|
|
||||||
|
|
||||||
import { MediaItem } from './components/media_item';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
|
||||||
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(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),
|
|
||||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class LoadMoreMedia extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
maxId: PropTypes.string,
|
|
||||||
onLoadMore: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLoadMore = () => {
|
|
||||||
this.props.onLoadMore(this.props.maxId);
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<LoadMore
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
onClick={this.handleLoadMore}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class AccountGallery extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
params: PropTypes.shape({
|
|
||||||
acct: PropTypes.string,
|
|
||||||
id: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
accountId: PropTypes.string,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
attachments: ImmutablePropTypes.list.isRequired,
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
hasMore: PropTypes.bool,
|
|
||||||
isAccount: PropTypes.bool,
|
|
||||||
blockedBy: PropTypes.bool,
|
|
||||||
suspended: PropTypes.bool,
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
width: 323,
|
|
||||||
};
|
|
||||||
|
|
||||||
_load () {
|
|
||||||
const { accountId, isAccount, dispatch } = this.props;
|
|
||||||
|
|
||||||
if (!isAccount) dispatch(fetchAccount(accountId));
|
|
||||||
dispatch(expandAccountMediaTimeline(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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScrollToBottom = () => {
|
|
||||||
if (this.props.hasMore) {
|
|
||||||
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleScroll = e => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
|
||||||
const offset = scrollHeight - scrollTop - clientHeight;
|
|
||||||
|
|
||||||
if (150 > offset && !this.props.isLoading) {
|
|
||||||
this.handleScrollToBottom();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLoadMore = maxId => {
|
|
||||||
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLoadOlder = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.handleScrollToBottom();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOpenMedia = attachment => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const statusId = attachment.getIn(['status', 'id']);
|
|
||||||
const lang = attachment.getIn(['status', 'language']);
|
|
||||||
|
|
||||||
if (attachment.get('type') === 'video') {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'VIDEO',
|
|
||||||
modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
|
|
||||||
}));
|
|
||||||
} else if (attachment.get('type') === 'audio') {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'AUDIO',
|
|
||||||
modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
const media = attachment.getIn(['status', 'media_attachments']);
|
|
||||||
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
|
|
||||||
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'MEDIA',
|
|
||||||
modalProps: { media, index, statusId, lang },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRef = c => {
|
|
||||||
if (c) {
|
|
||||||
this.setState({ width: c.offsetWidth });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
|
|
||||||
const { width } = this.state;
|
|
||||||
|
|
||||||
if (!isAccount) {
|
|
||||||
return (
|
|
||||||
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!attachments && isLoading) {
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadOlder = null;
|
|
||||||
|
|
||||||
if (hasMore && !(isLoading && attachments.size === 0)) {
|
|
||||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let emptyMessage;
|
|
||||||
|
|
||||||
if (suspended) {
|
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
|
||||||
} else if (blockedBy) {
|
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<ColumnBackButton />
|
|
||||||
|
|
||||||
<ScrollContainer scrollKey='account_gallery'>
|
|
||||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
|
||||||
<AccountHeader accountId={this.props.accountId} />
|
|
||||||
|
|
||||||
{(suspended || blockedBy) ? (
|
|
||||||
<div className='empty-column-indicator'>
|
|
||||||
{emptyMessage}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
|
||||||
{attachments.map((attachment, index) => attachment === null ? (
|
|
||||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
|
||||||
) : (
|
|
||||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{loadOlder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoading && attachments.size === 0 && (
|
|
||||||
<div className='scrollable__append'>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollContainer>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(AccountGallery);
|
|
||||||
|
|
@ -0,0 +1,283 @@
|
||||||
|
import { useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
|
||||||
|
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||||
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
|
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
||||||
|
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
|
||||||
|
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
|
||||||
|
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
import type { MediaAttachment } from 'mastodon/models/media_attachment';
|
||||||
|
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||||
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
|
import type { RootState } from 'mastodon/store';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { MediaItem } from './components/media_item';
|
||||||
|
|
||||||
|
const getAccountGallery = createSelector(
|
||||||
|
[
|
||||||
|
(state: RootState, accountId: string) =>
|
||||||
|
(state.timelines as ImmutableMap<string, unknown>).getIn(
|
||||||
|
[`account:${accountId}:media`, 'items'],
|
||||||
|
ImmutableList(),
|
||||||
|
) as ImmutableList<string>,
|
||||||
|
(state: RootState) => state.statuses,
|
||||||
|
],
|
||||||
|
(statusIds, statuses) => {
|
||||||
|
let items = ImmutableList<MediaAttachment>();
|
||||||
|
|
||||||
|
statusIds.forEach((statusId) => {
|
||||||
|
const status = statuses.get(statusId) as
|
||||||
|
| ImmutableMap<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
items = items.concat(
|
||||||
|
(
|
||||||
|
status.get('media_attachments') as ImmutableList<MediaAttachment>
|
||||||
|
).map((media) => media.set('status', status)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
acct?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RemoteHint: React.FC<{
|
||||||
|
accountId: string;
|
||||||
|
}> = ({ accountId }) => {
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
const acct = account?.acct;
|
||||||
|
const url = account?.url;
|
||||||
|
const domain = acct ? acct.split('@')[1] : undefined;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimelineHint
|
||||||
|
url={url}
|
||||||
|
message={
|
||||||
|
<FormattedMessage
|
||||||
|
id='hints.profiles.posts_may_be_missing'
|
||||||
|
defaultMessage='Some posts from this profile may be missing.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='hints.profiles.see_more_posts'
|
||||||
|
defaultMessage='See more posts on {domain}'
|
||||||
|
values={{ domain: <strong>{domain}</strong> }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AccountGallery: React.FC<{
|
||||||
|
multiColumn: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const { acct, id } = useParams<Params>();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const accountId = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
id ??
|
||||||
|
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
|
||||||
|
);
|
||||||
|
const attachments = useAppSelector((state) =>
|
||||||
|
accountId
|
||||||
|
? getAccountGallery(state, accountId)
|
||||||
|
: ImmutableList<MediaAttachment>(),
|
||||||
|
);
|
||||||
|
const isLoading = useAppSelector((state) =>
|
||||||
|
(state.timelines as ImmutableMap<string, unknown>).getIn([
|
||||||
|
`account:${accountId}:media`,
|
||||||
|
'isLoading',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const hasMore = useAppSelector((state) =>
|
||||||
|
(state.timelines as ImmutableMap<string, unknown>).getIn([
|
||||||
|
`account:${accountId}:media`,
|
||||||
|
'hasMore',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
|
);
|
||||||
|
const blockedBy = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.relationships.getIn([accountId, 'blocked_by'], false) as boolean,
|
||||||
|
);
|
||||||
|
const suspended = useAppSelector(
|
||||||
|
(state) => state.accounts.getIn([accountId, 'suspended'], false) as boolean,
|
||||||
|
);
|
||||||
|
const isAccount = !!account;
|
||||||
|
const remote = account?.acct !== account?.username;
|
||||||
|
const hidden = useAppSelector((state) =>
|
||||||
|
accountId ? getAccountHidden(state, accountId) : false,
|
||||||
|
);
|
||||||
|
const maxId = attachments.last()?.getIn(['status', 'id']) as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!accountId) {
|
||||||
|
dispatch(lookupAccount(acct));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId, acct]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId && !isAccount) {
|
||||||
|
dispatch(fetchAccount(accountId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountId && isAccount) {
|
||||||
|
void dispatch(expandAccountMediaTimeline(accountId));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId, isAccount]);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(() => {
|
||||||
|
if (maxId) {
|
||||||
|
void dispatch(expandAccountMediaTimeline(accountId, { maxId }));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId, maxId]);
|
||||||
|
|
||||||
|
const handleOpenMedia = useCallback(
|
||||||
|
(attachment: MediaAttachment) => {
|
||||||
|
const statusId = attachment.getIn(['status', 'id']);
|
||||||
|
const lang = attachment.getIn(['status', 'language']);
|
||||||
|
|
||||||
|
if (attachment.get('type') === 'video') {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'VIDEO',
|
||||||
|
modalProps: {
|
||||||
|
media: attachment,
|
||||||
|
statusId,
|
||||||
|
lang,
|
||||||
|
options: { autoPlay: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (attachment.get('type') === 'audio') {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'AUDIO',
|
||||||
|
modalProps: {
|
||||||
|
media: attachment,
|
||||||
|
statusId,
|
||||||
|
lang,
|
||||||
|
options: { autoPlay: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const media = attachment.getIn([
|
||||||
|
'status',
|
||||||
|
'media_attachments',
|
||||||
|
]) as ImmutableList<MediaAttachment>;
|
||||||
|
const index = media.findIndex(
|
||||||
|
(x) => x.get('id') === attachment.get('id'),
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'MEDIA',
|
||||||
|
modalProps: { media, index, statusId, lang },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accountId && !isAccount) {
|
||||||
|
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyMessage;
|
||||||
|
|
||||||
|
if (accountId) {
|
||||||
|
if (suspended) {
|
||||||
|
emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_suspended'
|
||||||
|
defaultMessage='Account suspended'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (hidden) {
|
||||||
|
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||||
|
} else if (blockedBy) {
|
||||||
|
emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_unavailable'
|
||||||
|
defaultMessage='Profile unavailable'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (remote && attachments.isEmpty()) {
|
||||||
|
emptyMessage = <RemoteHint accountId={accountId} />;
|
||||||
|
} else {
|
||||||
|
emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_timeline'
|
||||||
|
defaultMessage='No posts found'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const forceEmptyState = suspended || blockedBy || hidden;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<ColumnBackButton />
|
||||||
|
|
||||||
|
<ScrollableList
|
||||||
|
className='account-gallery__container'
|
||||||
|
prepend={
|
||||||
|
accountId && (
|
||||||
|
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
alwaysPrepend
|
||||||
|
append={remote && accountId && <RemoteHint accountId={accountId} />}
|
||||||
|
scrollKey='account_gallery'
|
||||||
|
isLoading={isLoading}
|
||||||
|
hasMore={!forceEmptyState && hasMore}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
>
|
||||||
|
{attachments.map((attachment) => (
|
||||||
|
<MediaItem
|
||||||
|
key={attachment.get('id') as string}
|
||||||
|
attachment={attachment}
|
||||||
|
onOpenMedia={handleOpenMedia}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default AccountGallery;
|
||||||
|
|
@ -6,7 +6,6 @@ import classNames from 'classnames';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { useLinks } from '@/hooks/useLinks';
|
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
|
@ -46,6 +45,7 @@ import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||||
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
|
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
|
||||||
import AccountNoteContainer from 'mastodon/features/account/containers/account_note_container';
|
import AccountNoteContainer from 'mastodon/features/account/containers/account_note_container';
|
||||||
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
|
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
|
||||||
|
import { useLinks } from 'mastodon/hooks/useLinks';
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
|
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
@ -58,8 +58,8 @@ import {
|
||||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import MemorialNote from './memorial_note';
|
import { MemorialNote } from './memorial_note';
|
||||||
import MovedNote from './moved_note';
|
import { MovedNote } from './moved_note';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
|
|
@ -833,7 +833,7 @@ export const AccountHeader: React.FC<{
|
||||||
<div className='account-timeline__header'>
|
<div className='account-timeline__header'>
|
||||||
{!hidden && account.memorial && <MemorialNote />}
|
{!hidden && account.memorial && <MemorialNote />}
|
||||||
{!hidden && account.moved && (
|
{!hidden && account.moved && (
|
||||||
<MovedNote from={account} to={account.moved} />
|
<MovedNote accountId={account.id} targetAccountId={account.moved} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue