Compare commits

..

No commits in common. "415c5eb2d474c9378b522c1359429b2c7997ddb1" and "63d7a30503c803f1960ca6a1ba6a33c6f9463c1b" have entirely different histories.

161 changed files with 2147 additions and 5559 deletions

View file

@ -1,40 +1,22 @@
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Order-independent
*.sw*
*.swp
*~
.DS_Store
.bundle
.env
.env.*
.git
.gitattributes
.github
.gitignore
.woodpecker.yml
build
chart
coverage
data
elasticsearch
log
neo4j
node_modules
postgres
postgres*
postgres14
.github
public/system
public/assets
public/packs
public/packs-test
public/system
redis
sorbet
tmp
node_modules
neo4j
vendor/bundle
.DS_Store
*.swp
*~
postgres
postgres14
redis
elasticsearch
chart

View file

@ -1,9 +0,0 @@
LOCAL_DOMAIN=localhost
ALTERNATE_DOMAINS=mastodon.internal
DB_HOST=$PWD/data/postgres
DB_USER=mastodon
DB_NAME=mastodon_dev
REDIS_URL=unix://$PWD/data/redis/redis-dev.sock
TH_USE_INVITE_QUOTA=1

View file

@ -14,7 +14,7 @@
# ----------
# This identifies your server and cannot be changed safely later
# ----------
LOCAL_DOMAIN=localhost
LOCAL_DOMAIN=example.com
# Use this only if you need to run mastodon on a different domain than the one used for federation.
# You can read more about this option on https://docs.joinmastodon.org/admin/config/#web-domain
@ -25,7 +25,6 @@ LOCAL_DOMAIN=localhost
# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
# be added. Comma separated values
# ALTERNATE_DOMAINS=example1.com,example2.com
ALTERNATE_DOMAINS=mastodon.internal
# Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118
@ -44,13 +43,13 @@ ALTERNATE_DOMAINS=mastodon.internal
# Redis
# -----
REDIS_HOST=redis
REDIS_HOST=localhost
REDIS_PORT=6379
# PostgreSQL
# ----------
DB_HOST=db
DB_HOST=/var/run/postgresql
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=
@ -270,9 +269,6 @@ MAX_POLL_OPTIONS=5
# Maximum allowed poll option characters
MAX_POLL_OPTION_CHARS=100
# Maximum number of emoji reactions per toot and user (minimum 1)
MAX_REACTIONS=1
# Maximum image and video/audio upload sizes
# Units are in bytes
# 1048576 bytes equals 1 megabyte

View file

@ -3,8 +3,3 @@ NODE_ENV=production
# Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true
DB_HOST=$(pwd)/data/postgres
DB_USER=mastodon
DB_NAME=mastodon_dev
REDIS_URL=unix://./data/redis/redis-dev.sock

View file

@ -48,7 +48,7 @@ jobs:
use_native_arm64_builder: false
cache: false
push_to_images: |
ghcr.io/${{ github.repository_owner }}/mastodon-streaming
ghcr.io/${{ github.repository_owner }}/mastodon
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes

15
.gitignore vendored
View file

@ -4,9 +4,6 @@
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore local dotenv overrides
.env.*.local
# Ignore bundler config and downloaded libraries.
/.bundle
/vendor/bundle
@ -15,9 +12,6 @@
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore local data directory
/data
# Ignore all logfiles and tempfiles.
.eslintcache
/log/*
@ -75,12 +69,3 @@ yarn-debug.log
# Ignore Docker option files
docker-compose.override.yml
# Yarn Berry
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View file

@ -1,82 +0,0 @@
variables:
environment: &docker-environment
NAME: gitea.treehouse.systems/treehouse/mastodon
DATE_COMMAND: export COMMIT_DATE=$(date -u -Idate -d @$(git show -s --format=%ct))
docker-step: &docker-step
image: docker:rc-git
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
<<: *docker-environment
clone:
git:
image: woodpeckerci/plugin-git
settings:
partial: false
depth: 10
pipeline:
# build-base:
# <<: *docker-step
# commands:
# - docker version
# - docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . --target build-base -t $NAME:build-base
# build:
# <<: *docker-step
# commands:
# - docker version
# - docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . --target build -t $NAME:build
# output-base:
# <<: *docker-step
# commands:
# - docker version
# - docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . --target output-base -t $NAME:build
# the world is not yet ready for this step
# test:
# <<: *docker-step
# commands:
# - docker run --rm -e RAILS_ENV=test -e NODE_ENV=development $NAME:build-base sh -c 'bundle config set --local without development && bundle install && rake spec'
output:
<<: *docker-step
commands:
- eval $DATE_COMMAND
- export TAG=$${COMMIT_DATE}.$CI_COMMIT_SHA && echo $${TAG}
- docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . -t $NAME:latest
- docker image build -f streaming/Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . -t $NAME-streaming:latest
- docker tag $NAME:latest $NAME:$TAG
- docker tag $NAME-streaming:latest $NAME-streaming:$TAG
# idk what's actually persisted between steps
# /shrug this works, so,???
- echo $${TAG} > tags.txt
- echo latest >> tags.txt
# maybe we can use tags someday,,,
# tag-tag:
# image: *docker-git
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock
# commands:
# - docker tag $NAME:latest $NAME:$CI_COMMIT_TAG
# when:
# event: tag
push:
<<: *docker-step
commands:
- echo $REGISTRY_SECRET | docker login -u $REGISTRY_USER --password-stdin gitea.treehouse.systems
- cat tags.txt | xargs -n 1 -I% echo docker image push $NAME:%
- cat tags.txt | xargs -n 1 -I% docker image push $NAME:%
- cat tags.txt | xargs -n 1 -I% echo docker image push $NAME-streaming:%
- cat tags.txt | xargs -n 1 -I% docker image push $NAME-streaming:%
when:
event: [push, tag]
branch: main
secrets: [REGISTRY_SECRET]
environment:
<<: *docker-environment
REGISTRY_USER: ariadne

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1 @@
enableGlobalCache: true
logFilters:
- code: YN0013
level: "${YARN_NOISE_LOG_CODE_LEVEL:-info}"
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.1.0.cjs

View file

@ -1,29 +1,11 @@
# Contributing to Mastodon Glitch+Treehouse Edition
# Contributing to Mastodon Glitch Edition
Thank you for your interest in contributing to the **Treehouse Mastodon** project!
Thank you for your interest in contributing to the `glitch-soc` project!
Here are some guidelines, and ways you can help.
> (This document is a bit of a work-in-progress, so please bear with us.
> If you don't see what you're looking for here, please don't hesitate to reach out!)
## Merging
If your username is kouhai, or you're otherwise merging from upstream glitch-soc
for some reason, the following snippets may be useful:
```sh
git fetch glitch && git merge glitch/main && git checkout glitch/main -- yarn.lock
```
```sh
export RAILS_ENV=production NODE_ENV=production
export OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder
bundle install \
&& yarn install \
&& bundle exec rake assets:clobber \
&& bundle exec rake webpacker:compile | tee /tmp/out.log
```
## Translations
You can submit glitch-soc-specific translations via [Crowdin](https://crowdin.com/project/glitch-soc). They are periodically merged into the codebase.
@ -32,19 +14,14 @@ You can submit glitch-soc-specific translations via [Crowdin](https://crowdin.co
## Planning
Right now a lot of the planning for this project takes place in the `#fediverse`
channel of the Treehouse Discord, or through Gitea Issues.
Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects.
We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler.
## Documentation
The upstream Glitch documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)).
## Setup
For a some-batteries-required guide to setting up a development environment for this repository, read Rin's excellent
[SETUP.md](https://gitea.treehouse.systems/treehouse/mastodon/src/branch/main/SETUP.md).
The documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)).
Right now, we've mostly focused on the features that make this fork different from upstream in some manner.
Adding screenshots, improving descriptions, and so forth are all ways to help contribute to the project even if you don't know any code.
## Frontend Development

View file

@ -7,16 +7,15 @@
ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM}
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"]
ARG RUBY_VERSION="3.2.2"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.3"]
ARG RUBY_VERSION="3.2.3"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
ARG NODE_MAJOR_VERSION="20"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm)
# Ruby image to use for base image based on combined variables (ex: 3.2.3-slim-bookworm)
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
@ -116,13 +115,13 @@ RUN \
# Create temporary build layer from base image
FROM ruby as build
COPY --from=node /usr/local/bin /usr/local/bin
COPY --from=node /usr/local/lib /usr/local/lib
# 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
# hadolint ignore=DL3008
@ -189,7 +188,7 @@ RUN \
--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 \
# Install Node packages
yarn workspaces focus --production @mastodon/mastodon
yarn workspaces focus --production @mastodon/mastodon;
# Create temporary assets build layer from build layer
FROM build as precompiler
@ -208,9 +207,7 @@ RUN \
# Use Ruby on Rails to create Mastodon assets
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \
# Cleanup temporary files
rm -fr /opt/mastodon/tmp; \
# TODO(kouhai): fork emoji-mart instead of patching
mv ./emoji_data/all.json ./node_modules/emoji-mart/data/all.json
rm -fr /opt/mastodon/tmp;
# Prep final Mastodon Ruby layer
FROM ruby as mastodon
@ -260,4 +257,4 @@ USER mastodon
# Expose default Puma ports
EXPOSE 3000
# Set container tini as default entry point
ENTRYPOINT ["/usr/bin/tini", "--"]
ENTRYPOINT ["/usr/bin/tini", "--"]

View file

@ -190,9 +190,6 @@ group :development, :test do
# RSpec runner for rails
gem 'rspec-rails', '~> 6.0'
# foreman
gem 'foreman'
end
group :production do

View file

@ -301,7 +301,6 @@ GEM
fog-openstack (1.1.0)
fog-core (~> 2.1)
fog-json (>= 1.0)
foreman (0.87.2)
formatador (1.1.0)
fugit (1.8.1)
et-orbi (~> 1, >= 1.2.7)
@ -859,7 +858,6 @@ DEPENDENCIES
fastimage
fog-core (<= 2.4.0)
fog-openstack (~> 1.0)
foreman
fuubar (~> 2.5)
haml-rails (~> 2.0)
haml_lint

View file

@ -2,4 +2,3 @@ web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
stream: env PORT=4000 yarn workspace @mastodon/streaming start
webpack: bin/webpack-dev-server
caddy: caddy run

View file

@ -1,22 +1,14 @@
# Mastodon Glitch+Treehouse Edition #
# Mastodon Glitch Edition
> Now with bunny ears!
> Now with automated deploys!
So here's the deal: we all work on this code, and anyone who uses that does so absolutely at their own risk. Can you dig it?
[![Build Status](https://img.shields.io/circleci/project/github/glitch-soc/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/glitch-soc/mastodon.svg)][code_climate]
Specifically, this fork-of-a-fork is intended for Treehouse use only. Unless
otherwise communicated, we will not put effort into supporting other deployments
or upstreaming our patches. This repo might be messy. Cleanliness of Git history
is not guaranteed -- watch out for falling rebases regardless of A-button count!
[circleci]: https://circleci.com/gh/glitch-soc/mastodon
[code_climate]: https://codeclimate.com/github/glitch-soc/mastodon
## Links
So here's the deal: we all work on this code, and anyone who uses that does so absolutely at their own risk. can you dig it?
- You can view upstream Glitch's documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
- Contributing guidelines are available [here](CONTRIBUTING.md).
## Known Deployments
- Treehouse Social: [social.treehouse.systems](https://social.treehouse.systems)
- VT Social: [vt.social](https://vt.social)
- Unstable Systems: [unstable.systems](https://unstable.systems)
- Slowest (dot) Network: [slowest.network](https://slowest.network)
- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/).

View file

@ -6,12 +6,3 @@
require File.expand_path('config/application', __dir__)
Rails.application.load_tasks
# please don't do this
if Rake::Task.task_defined?('assets:precompile') && ENV.include?('RAKE_NO_YARN_INSTALL_HACK')
task = Rake::Task['assets:precompile']
puts task.prerequisites
task.prerequisites.delete('webpacker:yarn_install')
task.prerequisites.delete('yarn:install')
puts task.prerequisites
end

143
SETUP.md
View file

@ -1,143 +0,0 @@
# Setting up a dev environment
## Prerequisites
Mastodon development requires the following:
- Ruby 3.0
- Ruby gems:
- `bundler`
- `irb`
- `foreman`
- NodeJS v18 (LTS)
- NPM packages:
- `yarn`
- Postgres
- Redis
### macOS
First, make sure you have Homebrew installed. Follow the instructions at [brew.sh](https://brew.sh).
Run the following to install all necessary packages:
```
brew install ruby@3.0 foreman node yarn postgresql redis
```
Ruby 3.0 is **keg-only** by default. Follow the instructions in the **Caveat** to add it to your path.
### Linux
We will assume that you know how to locate the correct packages for your distro. That said, some distros package `bundler` and `irb` separately. Make sure that you also install these.
On Arch, you will need:
- `ruby`
- `ruby-bundler`
- `ruby-irb`
- `ruby-foreman`
- `redis`
- `postgresql`
- `yarn`
- `gmp`
- `libidn`
### Windows
Unfortunately, none of the authors use Windows. Contributions welcome!
## Database
In the root of this repository, go through the following script:
```sh
# Create a folder for local data
mkdir -p data
# Set up a local database
pg_ctl -D data/postgres initdb -o '-U mastodon --auth-host=trust'
# Use the data/postgres folder for the DB connection unix socket
#
# If you don't know what that means, it's just a way for Mastodon to communicate
# with a database on the same machine efficiently.
#
# See: https://manpages.ubuntu.com/manpages/jammy/man7/unix.7.html
echo 'unix_socket_directories = .' >> data/postgres/postgresql.conf
# Start the database
pg_ctl -D data/postgres start --silent
```
## Redis
In the root of this repository, run the following:
```sh
# Create a folder for redis data
mkdir -p data/redis
# Start Redis
redis-server ./redis-dev.conf
# [Optional] Stop Redis
# kill "$(cat ./data/redis/redis-dev.pid)"
```
## Ruby
```sh
export RAILS_ENV=development
# Bundle installs all Ruby gems globally by default, which might cause problems.
bundle config set --local path 'vendor/bundle'
# [Apple Silicon] If using macOS on Apple Silicon, run the following:
# bundle config build.idn-ruby -- --with-idn-dir="$(brew --prefix libidn)"
# Install dependencies using bundle (Ruby) and yarn (JS)
bundle install
yarn install
# Setup the database using the pre-defined Rake task
#
# Rake is a command runner for Ruby projects. The `bundle exec` ensures that
# we use the version of Rake that this project requires.
bundle exec rake db:setup
# [Optional] If that fails, run the following and try again:
# bundle exec rake db:reset
```
## Running Mastodon
1. Run `export RAILS_ENV=development NODE_ENV=development`.
- Put these in your shell's .rc, or a script you can source if you want to skip this step in the future.
2. Run `bundle exec rake assets:precompile`.
- If this explodes, complaining about `Hash`, you'll need to `export NODE_OPTIONS=--openssl-legacy-provider`.
- After doing this, you will need to run `bundle exec rake assets:clobber` and then re-run `bundle exec rake assets:precompile`.
3. Run `foreman start`
# Updates/Troubleshooting
## RubyVM/DebugInspector Issues
Still unable to fix. Circumvent by removing `better_errors` and `binding_of_caller` from Gemfile.
Happy to troubleshoot with someone better with Ruby than us >_<'/.
## Webpack Issues
If Webpack compalins about being unable to find some assets or locales:
Try:
1. `rm -rf node_modules`
2. `yarn install`
If this doesn't help, try:
1. `yarn add webpack`
2. `git restore package.json yarn.lock`
3. `yarn install`
Then re-run `foreman start`. No. We have no idea why this worked.
# Need Help?
If the above instructions don't work, please contact @Rin here, or @tammy@social.treehouse.systems.

View file

@ -1,19 +0,0 @@
# frozen_string_literal: true
class Api::V1::Statuses::ReactionsController < Api::V1::Statuses::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
before_action :require_user!
def create
ReactService.new.call(current_account, @status, params[:id])
render json: @status, serializer: REST::StatusSerializer
end
def destroy
UnreactWorker.perform_async(current_account.id, @status.id, params[:id])
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reactions_map: { @status.id => false })
rescue Mastodon::NotPermittedError
not_found
end
end

View file

@ -69,9 +69,7 @@ class Api::V1::StatusesController < Api::BaseController
content_type: status_params[:content_type],
allowed_mentions: status_params[:allowed_mentions],
idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true,
quote_id: status_params[:quote_id].presence,
local_only: status_params[:local_only])
with_rate_limit: true
)
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
@ -142,10 +140,8 @@ class Api::V1::StatusesController < Api::BaseController
:visibility,
:language,
:scheduled_at,
:quote_id,
:content_type,
allowed_mentions: [],
:local_only,
media_ids: [],
media_attributes: [
:id,

View file

@ -15,8 +15,6 @@ class InvitesController < ApplicationController
@invites = invites
@invite = Invite.new
@invite.max_uses ||= 1
@invite.expires_in ||= 1.day.in_seconds
end
def create

View file

@ -33,7 +33,6 @@ module ContextHelper
'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText'
},
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' },
}.freeze
def full_context

View file

@ -19,17 +19,7 @@ module FormattingHelper
module_function :extract_status_plain_text
def status_content_format(status)
base = html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
if status.quote? && status.local?
after_html = begin
"<span class=\"quote-inline\"><a href=\"#{status.quote.to_log_permalink}\" class=\"status-link unhandled-link\" target=\"_blank\">#{status.quote.to_log_permalink}</a></span>"
end.html_safe # rubocop:disable Rails/OutputSafety
base + after_html
else
base
end
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
end
def rss_status_content_format(status)

View file

@ -86,9 +86,6 @@ export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
@ -145,25 +142,6 @@ export function cancelReplyCompose() {
};
}
export function quoteCompose(status, router) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_QUOTE,
status: status,
});
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/publish');
}
};
}
export function cancelQuoteCompose() {
return {
type: COMPOSE_QUOTE_CANCEL,
};
};
export function resetCompose() {
return {
type: COMPOSE_RESET,
@ -213,6 +191,10 @@ export function submitCompose(routerHistory) {
return;
}
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
status = status + ' 👁️';
}
dispatch(submitComposeRequest());
// If we're editing a post with media attachments, those have not
@ -242,7 +224,6 @@ export function submitCompose(routerHistory) {
status,
content_type: getState().getIn(['compose', 'content_type']),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
quote_id: getState().getIn(['compose', 'quote_id'], null),
media_ids: media.map(item => item.get('id')),
media_attributes,
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
@ -250,7 +231,6 @@ export function submitCompose(routerHistory) {
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
language: getState().getIn(['compose', 'language']),
local_only: getState().getIn(['compose', 'advanced_options', 'do_not_federate']),
},
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
@ -298,16 +278,12 @@ export function submitCompose(routerHistory) {
insertIfOnline('direct');
}
// Upstream shows a "post published" message every time you post now. This is highly annoying.
// Instead, only display a message if a draft is saved.
if (statusId !== null) {
dispatch(showAlert({
message: messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}
dispatch(showAlert({
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}).catch(function (error) {
dispatch(submitComposeFail(error));
});

View file

@ -48,8 +48,6 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.quote = normalOldStatus.get('quote');
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
if (normalOldStatus.get('translation')) {
normalStatus.translation = normalOldStatus.get('translation');
@ -63,35 +61,6 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
if (status.quote && status.quote.id) {
const quote_spoilerText = status.quote.spoiler_text || '';
const quote_searchContent = [quote_spoilerText, status.quote.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const quote_emojiMap = makeEmojiMap(normalStatus.quote.emojis);
const quote_account_emojiMap = makeEmojiMap(status.quote.account.emojis);
const displayName = normalStatus.quote.account.display_name.length === 0 ? normalStatus.quote.account.username : normalStatus.quote.account.display_name;
normalStatus.quote.account.display_name_html = emojify(escapeTextContentForBrowser(displayName), quote_account_emojiMap);
normalStatus.quote.search_index = domParser.parseFromString(quote_searchContent, 'text/html').documentElement.textContent;
let docElem = domParser.parseFromString(normalStatus.quote.content, 'text/html').documentElement;
Array.from(docElem.querySelectorAll('span.quote-inline'), span => span.remove());
Array.from(docElem.querySelectorAll('p,br'), line => {
let parentNode = line.parentNode;
if (line.nextSibling) {
parentNode.insertBefore(document.createTextNode(' '), line.nextSibling);
}
});
let _contentHtml = docElem.textContent;
normalStatus.quote.contentHtml = '<p>'+emojify(_contentHtml.substr(0, 150), quote_emojiMap) + (_contentHtml.substr(150) ? '...' : '')+'</p>';
normalStatus.quote.spoilerHtml = emojify(escapeTextContentForBrowser(quote_spoilerText), quote_emojiMap);
normalStatus.quote_hidden = (quote_spoilerText.length > 0 || normalStatus.quote.sensitive) && autoHideCW(settings, quote_spoilerText);
// delete the quote link!!!!
let parentDocElem = domParser.parseFromString(normalStatus.contentHtml, 'text/html').documentElement;
Array.from(parentDocElem.querySelectorAll('span.quote-inline'), span => span.remove());
normalStatus.contentHtml = parentDocElem.children[1].innerHTML;
}
}
if (normalOldStatus) {

View file

@ -51,16 +51,6 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
export const REACTION_UPDATE = 'REACTION_UPDATE';
export const REACTION_ADD_REQUEST = 'REACTION_ADD_REQUEST';
export const REACTION_ADD_SUCCESS = 'REACTION_ADD_SUCCESS';
export const REACTION_ADD_FAIL = 'REACTION_ADD_FAIL';
export const REACTION_REMOVE_REQUEST = 'REACTION_REMOVE_REQUEST';
export const REACTION_REMOVE_SUCCESS = 'REACTION_REMOVE_SUCCESS';
export const REACTION_REMOVE_FAIL = 'REACTION_REMOVE_FAIL';
export function reblog(status, visibility) {
return function (dispatch, getState) {
dispatch(reblogRequest(status));
@ -526,75 +516,3 @@ export function unpinFail(status, error) {
skipLoading: true,
};
}
export const addReaction = (statusId, name, url) => (dispatch, getState) => {
const status = getState().get('statuses').get(statusId);
let alreadyAdded = false;
if (status) {
const reaction = status.get('reactions').find(x => x.get('name') === name);
if (reaction && reaction.get('me')) {
alreadyAdded = true;
}
}
if (!alreadyAdded) {
dispatch(addReactionRequest(statusId, name, url));
}
// encodeURIComponent is required for the Keycap Number Sign emoji, see:
// <https://github.com/glitch-soc/mastodon/pull/1980#issuecomment-1345538932>
api(getState).post(`/api/v1/statuses/${statusId}/react/${encodeURIComponent(name)}`).then(() => {
dispatch(addReactionSuccess(statusId, name));
}).catch(err => {
if (!alreadyAdded) {
dispatch(addReactionFail(statusId, name, err));
}
});
};
export const addReactionRequest = (statusId, name, url) => ({
type: REACTION_ADD_REQUEST,
id: statusId,
name,
url,
});
export const addReactionSuccess = (statusId, name) => ({
type: REACTION_ADD_SUCCESS,
id: statusId,
name,
});
export const addReactionFail = (statusId, name, error) => ({
type: REACTION_ADD_FAIL,
id: statusId,
name,
error,
});
export const removeReaction = (statusId, name) => (dispatch, getState) => {
dispatch(removeReactionRequest(statusId, name));
api(getState).post(`/api/v1/statuses/${statusId}/unreact/${encodeURIComponent(name)}`).then(() => {
dispatch(removeReactionSuccess(statusId, name));
}).catch(err => {
dispatch(removeReactionFail(statusId, name, err));
});
};
export const removeReactionRequest = (statusId, name) => ({
type: REACTION_REMOVE_REQUEST,
id: statusId,
name,
});
export const removeReactionSuccess = (statusId, name) => ({
type: REACTION_REMOVE_SUCCESS,
id: statusId,
name,
});
export const removeReactionFail = (statusId, name) => ({
type: REACTION_REMOVE_FAIL,
id: statusId,
name,
});

View file

@ -142,7 +142,6 @@ const excludeTypesFromFilter = filter => {
'follow',
'follow_request',
'favourite',
'reaction',
'reblog',
'mention',
'poll',

View file

@ -20,7 +20,7 @@ import Card from '../features/status/components/card';
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { displayMedia, visibleReactions } from '../initial_state';
import { displayMedia } from '../initial_state';
import AttachmentList from './attachment_list';
import { getHashtagBarForStatus } from './hashtag_bar';
@ -29,7 +29,6 @@ import StatusContent from './status_content';
import StatusHeader from './status_header';
import StatusIcons from './status_icons';
import StatusPrepend from './status_prepend';
import StatusReactions from './status_reactions';
const domParser = new DOMParser();
@ -72,10 +71,6 @@ export const defaultMediaVisibility = (status, settings) => {
class Status extends ImmutablePureComponent {
static contextTypes = {
identity: PropTypes.object,
};
static propTypes = {
containerId: PropTypes.string,
id: PropTypes.string,
@ -86,15 +81,12 @@ class Status extends ImmutablePureComponent {
rootId: PropTypes.string,
onClick: PropTypes.func,
onReply: PropTypes.func,
onQuote: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onBookmark: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
onReactionAdd: PropTypes.func,
onReactionRemove: PropTypes.func,
onPin: PropTypes.func,
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
@ -732,7 +724,7 @@ class Status extends ImmutablePureComponent {
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
background = attachments.getIn([0, 'preview_url']);
}
} else if (!status.get('quote') && status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
media.push(
<Card
onOpenMedia={this.handleOpenMedia}
@ -759,7 +751,6 @@ class Status extends ImmutablePureComponent {
if (this.props.prepend && account) {
const notifKind = {
favourite: 'favourited',
reaction: 'reacted',
reblog: 'boosted',
reblogged_by: 'boosted',
status: 'posted',
@ -846,15 +837,6 @@ class Status extends ImmutablePureComponent {
{...statusContentProps}
/>
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
numVisible={visibleReactions}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={this.context.identity.signedIn}
/>
{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
<StatusActionBar
status={status}

View file

@ -8,10 +8,8 @@ import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AddReactionIcon from '@/material-icons/400-24px/add_reaction.svg?react';
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import FormatQuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
@ -28,8 +26,7 @@ import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
import { me, maxReactions } from '../initial_state';
import { me } from '../initial_state';
import { IconButton } from './icon_button';
import { RelativeTimestamp } from './relative_timestamp';
@ -48,11 +45,9 @@ const messages = defineMessages({
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
react: { id: 'status.react', defaultMessage: 'React' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
@ -81,9 +76,7 @@ class StatusActionBar extends ImmutablePureComponent {
status: ImmutablePropTypes.map.isRequired,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReactionAdd: PropTypes.func,
onReblog: PropTypes.func,
onQuote: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
@ -124,17 +117,6 @@ class StatusActionBar extends ImmutablePureComponent {
}
};
handleQuoteClick = () => {
const { signedIn } = this.context.identity;
if (signedIn) {
this.props.onQuote(this.props.status, this.props.history);
} else {
// TODO(ariadne): Add an interaction modal for quoting specifically.
this.props.onInteractionModal('reply', this.props.status);
}
};
handleShareClick = () => {
navigator.share({
url: this.props.status.get('url'),
@ -151,10 +133,6 @@ class StatusActionBar extends ImmutablePureComponent {
}
};
handleEmojiPick = data => {
this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl);
};
handleReblogClick = e => {
const { signedIn } = this.context.identity;
@ -230,8 +208,6 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onAddFilter(this.props.status);
};
handleNoOp = () => {}; // hack for reaction add button
render () {
const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
const { permissions, signedIn } = this.context.identity;
@ -244,7 +220,6 @@ class StatusActionBar extends ImmutablePureComponent {
let menu = [];
let reblogIcon = 'retweet';
let quoteIcon = 'quote-right';
let replyIcon;
let replyIconComponent;
let replyTitle;
@ -327,7 +302,6 @@ class StatusActionBar extends ImmutablePureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle, reblogIconComponent;
let quoteTitle, quoteIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
@ -343,35 +317,10 @@ class StatusActionBar extends ImmutablePureComponent {
reblogIconComponent = RepeatDisabledIcon;
}
// quotes
if (publicStatus) {
quoteTitle = intl.formatMessage(messages.quote);
quoteIconComponent = FormatQuoteIcon;
} else if (reblogPrivate) {
quoteTitle = intl.formatMessage(messages.reblog_private);
quoteIconComponent = FormatQuoteIcon;
} else {
quoteTitle = intl.formatMessage(messages.cannot_reblog);
quoteIconComponent = FormatQuoteIcon;
}
const filterButton = this.props.onFilter && (
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
);
const canReact = permissions && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;
const reactButton = (
<IconButton
className='status__action-bar-button'
onClick={this.handleNoOp} // EmojiPickerDropdown handles that
title={intl.formatMessage(messages.react)}
disabled={!canReact}
icon='add_reaction'
iconComponent={AddReactionIcon}
/>
);
return (
<div className='status__action-bar'>
<IconButton
@ -383,16 +332,8 @@ class StatusActionBar extends ImmutablePureComponent {
counter={showReplyCount ? status.get('replies_count') : undefined}
obfuscateCount
/>
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} /* active={status.get('reblogged')} */ title={quoteTitle} icon={quoteIcon} iconComponent={quoteIconComponent} onClick={this.handleQuoteClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
{
permissions
? <EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} />
: reactButton
}
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
{filterButton}

View file

@ -14,7 +14,6 @@ import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
import MovieIcon from '@/material-icons/400-24px/movie.svg?react';
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
import QuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
@ -364,37 +363,6 @@ class StatusContent extends PureComponent {
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
);
let quote = '';
if (status.get('quote', null) !== null) {
let quoteStatus = status.get('quote');
let quoteStatusContent = { __html: quoteStatus.get('contentHtml') };
let quoteStatusAccount = quoteStatus.get('account');
let quoteStatusDisplayName = { __html: quoteStatusAccount.get('display_name_html') };
quote = (
<div className='status__quote'>
<blockquote>
<bdi>
<span className='quote-display-name'>
<Icon
fixedWidth
aria-hidden='true'
key='icon-quote-right'
icon={QuoteIcon} />
<strong className='display-name__html'>
<a onClick={this.handleAccountClick} href={quoteStatus.getIn(['account', 'url'])} dangerouslySetInnerHTML={quoteStatusDisplayName} />
</strong>
</span>
</bdi>
<div>
<a href={quoteStatus.get('url')} target='_blank' rel='noopener noreferrer' dangerouslySetInnerHTML={quoteStatusContent} />
</div>
</blockquote>
</div>
);
}
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';
@ -469,7 +437,6 @@ class StatusContent extends PureComponent {
{mentionsPlaceholder}
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
{quote}
<div
ref={this.setContentsRef}
key={`contents-${tagLinks}`}
@ -495,7 +462,6 @@ class StatusContent extends PureComponent {
onMouseUp={this.handleMouseUp}
tabIndex={0}
>
{quote}
<div
ref={this.setContentsRef}
key={`contents-${tagLinks}-${rewriteMentions}`}
@ -517,7 +483,6 @@ class StatusContent extends PureComponent {
className='status__content'
tabIndex={0}
>
{quote}
<div
ref={this.setContentsRef}
key={`contents-${tagLinks}`}

View file

@ -6,7 +6,6 @@ import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import AddReactionIcon from '@/material-icons/400-24px/add_reaction.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
@ -67,14 +66,6 @@ export default class StatusPrepend extends PureComponent {
values={{ name : link }}
/>
);
case 'reaction':
return (
<FormattedMessage
id='notification.reaction'
defaultMessage='{name} reacted to your status'
values={{ name: link }}
/>
);
case 'reblog':
return (
<FormattedMessage
@ -130,10 +121,6 @@ export default class StatusPrepend extends PureComponent {
iconId = 'star';
iconComponent = StarIcon;
break;
case 'reaction':
iconId = 'add_reaction';
iconComponent = AddReactionIcon;
break;
case 'featured':
iconId = 'thumb-tack';
iconComponent = PushPinIcon;

View file

@ -1,175 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
import { autoPlayGif, reduceMotion } from '../initial_state';
import { assetHost } from '../utils/config';
import { AnimatedNumber } from './animated_number';
export default class StatusReactions extends ImmutablePureComponent {
static propTypes = {
statusId: PropTypes.string.isRequired,
reactions: ImmutablePropTypes.list.isRequired,
numVisible: PropTypes.number,
addReaction: PropTypes.func.isRequired,
canReact: PropTypes.bool.isRequired,
removeReaction: PropTypes.func.isRequired,
};
willEnter() {
return { scale: reduceMotion ? 1 : 0 };
}
willLeave() {
return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
}
render() {
const { reactions, numVisible } = this.props;
let visibleReactions = reactions
.filter(x => x.get('count') > 0)
.sort((a, b) => b.get('count') - a.get('count'));
if (numVisible >= 0) {
visibleReactions = visibleReactions.filter((_, i) => i < numVisible);
}
const styles = visibleReactions.map(reaction => ({
key: reaction.get('name'),
data: reaction,
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
})).toArray();
return (
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
{items => (
<div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
{items.map(({ key, data, style }) => (
<Reaction
key={key}
statusId={this.props.statusId}
reaction={data}
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
addReaction={this.props.addReaction}
removeReaction={this.props.removeReaction}
canReact={this.props.canReact}
/>
))}
</div>
)}
</TransitionMotion>
);
}
}
class Reaction extends ImmutablePureComponent {
static propTypes = {
statusId: PropTypes.string,
reaction: ImmutablePropTypes.map.isRequired,
addReaction: PropTypes.func.isRequired,
removeReaction: PropTypes.func.isRequired,
canReact: PropTypes.bool.isRequired,
style: PropTypes.object,
};
state = {
hovered: false,
};
handleClick = () => {
const { reaction, statusId, addReaction, removeReaction } = this.props;
if (reaction.get('me')) {
removeReaction(statusId, reaction.get('name'));
} else {
addReaction(statusId, reaction.get('name'));
}
};
handleMouseEnter = () => this.setState({ hovered: true });
handleMouseLeave = () => this.setState({ hovered: false });
render() {
const { reaction } = this.props;
return (
<button
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
disabled={!this.props.canReact}
style={this.props.style}
>
<span className='reactions-bar__item__emoji'>
<Emoji
hovered={this.state.hovered}
emoji={reaction.get('name')}
url={reaction.get('url')}
staticUrl={reaction.get('static_url')}
/>
</span>
<span className='reactions-bar__item__count'>
<AnimatedNumber value={reaction.get('count')} />
</span>
</button>
);
}
}
class Emoji extends React.PureComponent {
static propTypes = {
emoji: PropTypes.string.isRequired,
hovered: PropTypes.bool.isRequired,
url: PropTypes.string,
staticUrl: PropTypes.string,
};
render() {
const { emoji, hovered, url, staticUrl } = this.props;
if (unicodeMapping[emoji]) {
const { filename, shortCode } = unicodeMapping[this.props.emoji];
const title = shortCode ? `:${shortCode}:` : '';
return (
<img
draggable='false'
className='emojione'
alt={emoji}
title={title}
src={`${assetHost}/emoji/${filename}.svg`}
/>
);
} else {
const filename = (autoPlayGif || hovered) ? url : staticUrl;
const shortCode = `:${emoji}:`;
return (
<img
draggable='false'
className='emojione custom-emoji'
alt={shortCode}
title={shortCode}
src={filename}
/>
);
}
}
}

View file

@ -6,7 +6,6 @@ import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initBoostModal } from 'flavours/glitch/actions/boosts';
import {
replyCompose,
quoteCompose,
mentionCompose,
directCompose,
} from 'flavours/glitch/actions/compose';
@ -22,8 +21,6 @@ import {
unbookmark,
pin,
unpin,
addReaction,
removeReaction,
} from 'flavours/glitch/actions/interactions';
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
import { openModal } from 'flavours/glitch/actions/modal';
@ -55,8 +52,6 @@ const messages = defineMessages({
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
@ -119,26 +114,6 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
});
},
onQuote (status, router) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)),
onConfirm: () => dispatch(quoteCompose(status, router)),
},
}));
} else {
dispatch(quoteCompose(status, router));
}
});
},
onModalReblog (status, privacy) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
@ -198,14 +173,6 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}
},
onReactionAdd (statusId, name, url) {
dispatch(addReaction(statusId, name, url));
},
onReactionRemove (statusId, name) {
dispatch(removeReaction(statusId, name));
},
onEmbed (status) {
dispatch(openModal({
modalType: 'EMBED',

View file

@ -19,7 +19,6 @@ import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import OptionsContainer from '../containers/options_container';
import PollFormContainer from '../containers/poll_form_container';
import QuoteIndicatorContainer from '../containers/quote_indicator_container';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import UploadFormContainer from '../containers/upload_form_container';
import WarningContainer from '../containers/warning_container';
@ -278,7 +277,6 @@ class ComposeForm extends ImmutablePureComponent {
<WarningContainer />
<ReplyIndicatorContainer />
<QuoteIndicatorContainer />
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput

View file

@ -45,8 +45,6 @@ const notFoundFn = () => (
set='twitter'
size={32}
sheetSize={32}
sheetColumns={60}
sheetRows={60}
backgroundImageFn={backgroundImageFn}
/>
@ -105,12 +103,12 @@ class ModifierPickerMenu extends PureComponent {
return (
<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} sheetColumns={60} sheetRows={60} 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} sheetColumns={60} sheetRows={60} 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} sheetColumns={60} sheetRows={60} 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} sheetColumns={60} sheetRows={60} 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} sheetColumns={60} sheetRows={60} 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} sheetColumns={60} sheetRows={60} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<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={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={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={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={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={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
</div>
);
}
@ -145,7 +143,7 @@ class ModifierPicker extends PureComponent {
return (
<div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
<Emoji 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} />
</div>
);
@ -282,8 +280,6 @@ class EmojiPickerMenuImpl extends PureComponent {
perLine={8}
emojiSize={22}
sheetSize={32}
sheetColumns={60}
sheetRows={60}
custom={buildCustomEmojis(custom_emojis)}
color=''
emoji=''
@ -328,7 +324,6 @@ class EmojiPickerDropdown extends PureComponent {
onSkinTone: PropTypes.func.isRequired,
skinTone: PropTypes.number.isRequired,
button: PropTypes.node,
disabled: PropTypes.bool,
};
state = {
@ -362,7 +357,7 @@ class EmojiPickerDropdown extends PureComponent {
};
onToggle = (e) => {
if (!this.state.disabled && !this.state.loading && (!e.key || e.key === 'Enter')) {
if (!this.state.loading && (!e.key || e.key === 'Enter')) {
if (this.state.active) {
this.onHideDropdown();
} else {
@ -400,7 +395,7 @@ class EmojiPickerDropdown extends PureComponent {
/>}
</div>
<Overlay show={active} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
<Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
{({ props, placement })=> (
<div {...props} style={{ ...props.style, width: 299 }}>
<div className={`dropdown-animation ${placement}`}>

View file

@ -1,85 +0,0 @@
// Package imports.
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import AttachmentList from 'flavours/glitch/components/attachment_list';
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name';
import { Icon } from '../../../components/icon';
import { IconButton } from '../../../components/icon_button';
// Messages.
const messages = defineMessages({
cancel: {
defaultMessage: 'Cancel',
id: 'quote_indicator.cancel',
},
});
class QuoteIndicator extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
onCancel: PropTypes.func,
intl: PropTypes.object.isRequired,
...WithOptionalRouterPropTypes,
};
handleClick = () => {
this.props.onCancel();
};
handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.props.history?.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
}
}
// Rendering.
render () {
const { status, intl } = this.props;
if (!status) {
return null;
}
const content = { __html: status.get('contentHtml') };
// The result.
return (
<article className='quote-indicator'>
<header className='quote-indicator__header'>
<div className='quote-indicator__cancel'>
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted />
</div>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='quote-indicator__display-name' target='_blank' rel='noopener noreferrer'>
<div className='quote-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
<DisplayName account={status.get('account')} inline />
</a>
</header>
<div className='quote-indicator__content translate' dangerouslySetInnerHTML={content} />
{status.get('media_attachments').size > 0 && (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
)}
</article>
);
}
}
export default withOptionalRouter(injectIntl(QuoteIndicator));

View file

@ -12,7 +12,6 @@ import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch
import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name';
import { Icon } from '../../../components/icon';
import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({
@ -51,9 +50,7 @@ class ReplyIndicator extends ImmutablePureComponent {
return (
<div className='reply-indicator'>
<div className='reply-indicator__header'>
<div className='reply-indicator__cancel'>
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted />
</div>
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted /></div>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' target='_blank' rel='noopener noreferrer'>
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>

View file

@ -1,31 +0,0 @@
import { connect } from 'react-redux';
import { cancelQuoteCompose } from 'flavours/glitch/actions/compose';
import { makeGetStatus } from '../../../selectors';
import QuoteIndicator from '../components/quote_indicator';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = state => {
const statusId = state.getIn(['compose', 'quote_id'], null);
const editing = false;
return {
status: getStatus(state, { id: statusId }),
editing,
};
};
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
onCancel () {
dispatch(cancelQuoteCompose());
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(QuoteIndicator);

View file

@ -120,17 +120,6 @@ export default class ColumnSettings extends PureComponent {
</div>
</div>
<div role='group' aria-labelledby='notifications-reaction'>
<span id='notifications-reaction' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></span>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reaction']} onChange={onChange} label={alertStr} />
{showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reaction']} onChange={this.onPushChange} label={pushStr} />}
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reaction']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reaction']} onChange={onChange} label={soundStr} />
</div>
</div>
<div role='group' aria-labelledby='notifications-mention'>
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>

View file

@ -3,7 +3,6 @@ import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AddReactionIcon from '@/material-icons/400-24px/add_reaction.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
@ -16,7 +15,6 @@ import { Icon } from 'flavours/glitch/components/icon';
const tooltips = defineMessages({
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' },
reactions: { id: 'notifications.filter.reactions', defaultMessage: 'Reactions' },
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
@ -84,13 +82,6 @@ class FilterBar extends PureComponent {
>
<Icon id='star' icon={StarIcon} />
</button>
<button
className={selectedFilter === 'reaction' ? 'active' : ''}
onClick={this.onClick('reaction')}
title={intl.formatMessage(tooltips.reactions)}
>
<Icon id='add_reaction' icon={AddReactionIcon} />
</button>
<button
className={selectedFilter === 'reblog' ? 'active' : ''}
onClick={this.onClick('reblog')}

View file

@ -159,28 +159,6 @@ export default class Notification extends ImmutablePureComponent {
unread={this.props.unread}
/>
);
case 'reaction':
return (
<StatusContainer
containerId={notification.get('id')}
hidden={hidden}
id={notification.get('status')}
account={notification.get('account')}
prepend='reaction'
muted
notification={notification}
onMoveDown={onMoveDown}
onMoveUp={onMoveUp}
onMention={onMention}
getScrollPosition={getScrollPosition}
updateScrollBottom={updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
onUnmount={this.props.onUnmount}
withDismiss
unread={this.props.unread}
/>
);
case 'reblog':
return (
<StatusContainer

View file

@ -8,11 +8,9 @@ import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import AddReactionIcon from '@/material-icons/400-24px/add_reaction.svg?react';
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import QuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
@ -28,8 +26,7 @@ import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import { IconButton } from '../../../components/icon_button';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { me, maxReactions } from '../../../initial_state';
import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
import { me } from '../../../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -40,11 +37,9 @@ const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
react: { id: 'status.react', defaultMessage: 'React' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
more: { id: 'status.more', defaultMessage: 'More' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
@ -74,13 +69,11 @@ class ActionBar extends PureComponent {
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
onReactionAdd: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onQuote: PropTypes.func.isRequired,
onMute: PropTypes.func,
onBlock: PropTypes.func,
onMuteConversation: PropTypes.func,
@ -103,18 +96,10 @@ class ActionBar extends PureComponent {
this.props.onFavourite(this.props.status, e);
};
handleEmojiPick = data => {
this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl);
};
handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e);
};
handleQuoteClick = () => {
this.props.onQuote(this.props.status);
}
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.props.history);
};
@ -170,8 +155,6 @@ class ActionBar extends PureComponent {
navigator.clipboard.writeText(url);
};
handleNoOp = () => {}; // hack for reaction add button
render () {
const { status, intl } = this.props;
const { signedIn, permissions } = this.context.identity;
@ -248,18 +231,6 @@ class ActionBar extends PureComponent {
replyIconComponent = ReplyAllIcon;
}
const canReact = signedIn && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;
const reactButton = (
<IconButton
className='add-reaction-icon'
onClick={this.handleNoOp} // EmojiPickerDropdown handles that
title={intl.formatMessage(messages.react)}
disabled={!canReact}
icon='add_reaction'
iconComponent={AddReactionIcon}
/>
);
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle, reblogIconComponent;
@ -282,15 +253,7 @@ class ActionBar extends PureComponent {
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={replyIcon} iconComponent={replyIconComponent} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='quote-right-icon' disabled={!publicStatus} title={intl.formatMessage(messages.quote)} icon='quote-right' iconComponent={QuoteIcon} onClick={this.handleQuoteClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'>
{
signedIn
? <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} />
: reactButton
}
</div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__action-bar-dropdown'>

View file

@ -25,7 +25,6 @@ import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name';
import MediaGallery from '../../../components/media_gallery';
import StatusContent from '../../../components/status_content';
import StatusReactions from '../../../components/status_reactions';
import Audio from '../../audio';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import Video from '../../video';
@ -34,10 +33,6 @@ import Card from './card';
class DetailedStatus extends ImmutablePureComponent {
static contextTypes = {
identity: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map,
settings: ImmutablePropTypes.map.isRequired,
@ -56,8 +51,6 @@ class DetailedStatus extends ImmutablePureComponent {
available: PropTypes.bool,
}),
onToggleMediaVisibility: PropTypes.func,
onReactionAdd: PropTypes.func.isRequired,
onReactionRemove: PropTypes.func.isRequired,
...WithRouterPropTypes,
};
@ -235,7 +228,7 @@ class DetailedStatus extends ImmutablePureComponent {
);
mediaIcons.push('picture-o');
}
} else if (!status.get('quote') && status.get('card')) {
} else if (status.get('card')) {
media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />);
mediaIcons.push('link');
}
@ -336,14 +329,6 @@ class DetailedStatus extends ImmutablePureComponent {
{...statusContentProps}
/>
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={this.context.identity.signedIn}
/>
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />

View file

@ -7,7 +7,6 @@ import { initBlockModal } from '../../../actions/blocks';
import { initBoostModal } from '../../../actions/boosts';
import {
replyCompose,
quoteCompose,
mentionCompose,
directCompose,
} from '../../../actions/compose';
@ -34,8 +33,6 @@ import DetailedStatus from '../components/detailed_status';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
@ -74,21 +71,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
});
},
onQuote (status, router) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onConfirm: () => dispatch(quoteCompose(status, router)),
}));
} else {
dispatch(quoteCompose(status, router));
}
});
},
onModalReblog (status, privacy) {
dispatch(reblog(status, privacy));
},

View file

@ -28,7 +28,6 @@ import { initBlockModal } from '../../actions/blocks';
import { initBoostModal } from '../../actions/boosts';
import {
replyCompose,
quoteCompose,
mentionCompose,
directCompose,
} from '../../actions/compose';
@ -41,8 +40,6 @@ import {
unreblog,
pin,
unpin,
addReaction,
removeReaction,
} from '../../actions/interactions';
import { changeLocalSetting } from '../../actions/local_settings';
import { openModal } from '../../actions/modal';
@ -311,19 +308,6 @@ class Status extends ImmutablePureComponent {
}
};
handleReactionAdd = (statusId, name, url) => {
const { dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
dispatch(addReaction(statusId, name, url));
}
};
handleReactionRemove = (statusId, name) => {
this.props.dispatch(removeReaction(statusId, name));
};
handlePin = (status) => {
if (status.get('pinned')) {
this.props.dispatch(unpin(status));
@ -362,21 +346,6 @@ class Status extends ImmutablePureComponent {
}
};
handleQuoteClick = (status) => {
const { dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
dispatch(quoteCompose(status, this.context.router.history));
} else {
dispatch(openModal('INTERACTION', {
type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
}));
}
}
handleModalReblog = (status, privacy) => {
const { dispatch } = this.props;
@ -779,8 +748,6 @@ class Status extends ImmutablePureComponent {
settings={settings}
onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia}
onReactionAdd={this.handleReactionAdd}
onReactionRemove={this.handleReactionRemove}
expanded={isExpanded}
onToggleHidden={this.handleToggleHidden}
onTranslate={this.handleTranslate}
@ -795,10 +762,8 @@ class Status extends ImmutablePureComponent {
status={status}
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onReactionAdd={this.handleReactionAdd}
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onQuote={this.handleQuoteClick}
onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}

View file

@ -24,7 +24,6 @@
* @property {boolean} limited_federation_mode
* @property {string} locale
* @property {string | null} mascot
* @property {number} max_reactions
* @property {string=} me
* @property {string=} moved_to_account_id
* @property {string=} owner
@ -45,7 +44,6 @@
* @property {boolean} use_blurhash
* @property {boolean=} use_pending_items
* @property {string} version
* @property {number} visible_reactions
* @property {string} sso_redirect
* @property {boolean} translation_enabled
* @property {string} status_page_url
@ -70,7 +68,6 @@ export const hasMultiColumnPath = initialPath === '/'
* @property {object} local_settings
* @property {number} max_toot_chars
* @property {number} poll_limits
* @property {number} max_reactions
*/
const element = document.getElementById('initial-state');
@ -107,7 +104,6 @@ export const expandSpoilers = getMeta('expand_spoilers');
export const forceSingleColumn = !getMeta('advanced_layout');
export const limitedFederationMode = getMeta('limited_federation_mode');
export const mascot = getMeta('mascot');
export const maxReactions = (initialState && initialState.max_reactions) || 1;
export const me = getMeta('me');
export const movedToAccountId = getMeta('moved_to_account_id');
export const owner = getMeta('owner');
@ -127,7 +123,6 @@ export const unfollowModal = getMeta('unfollow_modal');
export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const visibleReactions = getMeta('visible_reactions');
export const languages = initialState?.languages;
export const criticalUpdatesPending = initialState?.critical_updates_pending;
export const statusPageUrl = getMeta('status_page_url');

View file

@ -60,14 +60,11 @@
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
"navigation_bar.misc": "Misc",
"notification.markForDeletion": "Mark for deletion",
"notification.reaction": "{name} reacted to your post",
"notification_purge.btn_all": "Select\nall",
"notification_purge.btn_apply": "Clear\nselected",
"notification_purge.btn_invert": "Invert\nselection",
"notification_purge.btn_none": "Select\nnone",
"notification_purge.start": "Enter notification cleaning mode",
"notifications.column_settings.reaction": "Reactions:",
"notifications.filter.reactions": "Reactions",
"notifications.marked_clear": "Clear selected notifications",
"notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
"settings.always_show_spoilers_field": "Always enable the Content Warning field",
@ -157,7 +154,6 @@
"status.in_reply_to": "This toot is a reply",
"status.is_poll": "This toot is a poll",
"status.local_only": "Only visible from your instance",
"status.react": "React",
"status.sensitive_toggle": "Click to view",
"status.uncollapse": "Uncollapse",
"suggestions.dismiss": "Dismiss suggestion"

View file

@ -8,8 +8,6 @@ import {
COMPOSE_REPLY,
COMPOSE_REPLY_CANCEL,
COMPOSE_DIRECT,
COMPOSE_QUOTE,
COMPOSE_QUOTE_CANCEL,
COMPOSE_MENTION,
COMPOSE_SUBMIT_REQUEST,
COMPOSE_SUBMIT_SUCCESS,
@ -89,7 +87,6 @@ const initialState = ImmutableMap({
caretPosition: null,
preselectDate: null,
in_reply_to: null,
quote_id: null,
is_submitting: false,
is_uploading: false,
is_changing_upload: false,
@ -178,7 +175,6 @@ function clearAll(state) {
map.set('is_submitting', false);
map.set('is_changing_upload', false);
map.set('in_reply_to', null);
map.set('quote_id', null);
map.update(
'advanced_options',
map => map.mergeWith(overwrite, state.get('default_advanced_options')),
@ -208,7 +204,7 @@ function continueThread (state, status) {
map.set('in_reply_to', status.id);
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: !!status.local_only }))
map => map.merge(new ImmutableMap({ do_not_federate: status.local_only })),
);
map.set('privacy', status.visibility);
map.set('sensitive', false);
@ -367,51 +363,6 @@ const updateSuggestionTags = (state, token) => {
});
};
const updateWithReply = (state, action) => {
// doesn't support QT&reply
const isQuote = action.type === COMPOSE_QUOTE;
const parentStatusId = action.status.get('id');
return state.withMutations(map => {
map.set('id', null);
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') })),
);
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
if (action.status.get('spoiler_text').length > 0) {
let spoilerText = action.status.get('spoiler_text');
if (action.prependCWRe && !spoilerText.match(/^(re|qt)[: ]/i)) {
spoilerText = isQuote ? `QT: ${spoilerText}` : `re: ${spoilerText}`;
}
map.set('spoiler', true);
map.set('spoiler_text', spoilerText);
} else {
map.set('spoiler', false);
map.set('spoiler_text', '');
}
if (isQuote) {
map.set('in_reply_to', null);
map.set('quote_id', parentStatusId);
map.set('text', '');
} else {
map.set('in_reply_to', parentStatusId);
map.set('quote_id', null);
map.set('text', statusToTextMentions(state, action.status));
if (action.status.get('language')) {
map.set('language', action.status.get('language'));
}
}
});
};
export default function compose(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
@ -461,17 +412,46 @@ export default function compose(state = initialState, action) {
return state
.set('elefriend', (state.get('elefriend') + 1) % totalElefriends);
case COMPOSE_REPLY:
case COMPOSE_QUOTE:
return updateWithReply(state, action);
return state.withMutations(map => {
map.set('id', null);
map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status));
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') })),
);
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
map.update('media_attachments', list => list.filter(media => media.get('unattached')));
if (action.status.get('language') && !action.status.has('translation')) {
map.set('language', action.status.get('language'));
} else {
map.set('language', state.get('default_language'));
}
if (action.status.get('spoiler_text').length > 0) {
let spoiler_text = action.status.get('spoiler_text');
if (action.prependCWRe && !spoiler_text.match(/^re[: ]/i)) {
spoiler_text = 're: '.concat(spoiler_text);
}
map.set('spoiler', true);
map.set('spoiler_text', spoiler_text);
} else {
map.set('spoiler', false);
map.set('spoiler_text', '');
}
});
case COMPOSE_REPLY_CANCEL:
state = state.setIn(['advanced_options', 'threaded_mode'], false);
// eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended
case COMPOSE_QUOTE_CANCEL:
// eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended
case COMPOSE_RESET:
return state.withMutations(map => {
map.set('in_reply_to', null);
map.set('quote_id', null);
if (defaultContentType) map.set('content_type', defaultContentType);
map.set('text', '');
map.set('spoiler', false);

View file

@ -38,7 +38,6 @@ const initialState = ImmutableMap({
follow: false,
follow_request: false,
favourite: false,
reaction: false,
reblog: false,
mention: false,
poll: false,
@ -61,7 +60,6 @@ const initialState = ImmutableMap({
follow: true,
follow_request: false,
favourite: true,
reaction: true,
reblog: true,
mention: true,
poll: true,
@ -75,7 +73,6 @@ const initialState = ImmutableMap({
follow: true,
follow_request: false,
favourite: true,
reaction: true,
reblog: true,
mention: true,
poll: true,

View file

@ -15,11 +15,6 @@ import {
BOOKMARK_FAIL,
UNBOOKMARK_REQUEST,
UNBOOKMARK_FAIL,
REACTION_UPDATE,
REACTION_ADD_FAIL,
REACTION_REMOVE_FAIL,
REACTION_ADD_REQUEST,
REACTION_REMOVE_REQUEST,
} from '../actions/interactions';
import {
STATUS_MUTE_SUCCESS,
@ -47,43 +42,6 @@ const deleteStatus = (state, id, references) => {
return state.delete(id);
};
const updateReaction = (state, id, name, updater) => state.update(
id,
status => status.update(
'reactions',
reactions => {
const index = reactions.findIndex(reaction => reaction.get('name') === name);
if (index > -1) {
return reactions.update(index, reaction => updater(reaction));
} else {
return reactions.push(updater(fromJS({ name, count: 0 })));
}
},
),
);
const updateReactionCount = (state, reaction) => updateReaction(state, reaction.status_id, reaction.name, x => x.set('count', reaction.count));
// The url parameter is only used when adding a new custom emoji reaction
// (one that wasn't in the reactions list before) because we don't have its
// URL yet. In all other cases, it's undefined.
const addReaction = (state, id, name, url) => updateReaction(
state,
id,
name,
x => x.set('me', true)
.update('count', n => n + 1)
.update('url', old => old ? old : url)
.update('static_url', old => old ? old : url),
);
const removeReaction = (state, id, name) => updateReaction(
state,
id,
name,
x => x.set('me', false).update('count', n => n - 1),
);
const statusTranslateSuccess = (state, id, translation) => {
return state.withMutations(map => {
map.setIn([id, 'translation'], fromJS(normalizeStatusTranslation(translation, map.get(id))));
@ -137,14 +95,6 @@ export default function statuses(state = initialState, action) {
return state.setIn([action.status.get('id'), 'reblogged'], true);
case REBLOG_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false);
case REACTION_UPDATE:
return updateReactionCount(state, action.reaction);
case REACTION_ADD_REQUEST:
case REACTION_REMOVE_FAIL:
return addReaction(state, action.id, action.name, action.url);
case REACTION_REMOVE_REQUEST:
case REACTION_ADD_FAIL:
return removeReaction(state, action.id, action.name);
case UNREBLOG_REQUEST:
return state.setIn([action.status.get('id'), 'reblogged'], false);
case UNREBLOG_FAIL:

View file

@ -327,10 +327,6 @@
text-align: center;
}
.detailed-status__button .emoji-button {
padding: 0;
}
.relationship-tag {
color: $white;
margin-bottom: 4px;

View file

@ -128,7 +128,6 @@
}
}
.quote-indicator,
.reply-indicator {
margin: 0 0 10px;
border-radius: 4px;
@ -139,19 +138,16 @@
flex: 0 2 auto;
}
.quote-indicator__header,
.reply-indicator__header {
margin-bottom: 5px;
overflow: hidden;
}
.quote-indicator__cancel,
.reply-indicator__cancel {
float: right;
line-height: 24px;
}
.quote-indicator__display-name,
.reply-indicator__display-name {
color: $inverted-text-color;
display: block;
@ -166,13 +162,11 @@
}
}
.quote-indicator__display-avatar,
.reply-indicator__display-avatar {
float: left;
margin-inline-end: 5px;
}
.quote-indicator__content,
.reply-indicator__content {
position: relative;
font-size: 14px;

View file

@ -68,8 +68,7 @@
}
p,
pre,
blockquote {
pre {
margin-bottom: 20px;
white-space: pre-wrap;
unicode-bidi: plaintext;
@ -79,99 +78,6 @@
}
}
.status__quote {
padding-bottom: 0.5em;
}
.status__quote,
.status__content__text,
.e-content {
overflow: hidden;
& > ul,
& > ol {
margin-bottom: 20px;
}
h1,
h2,
h3,
h4,
h5 {
margin-top: 20px;
margin-bottom: 20px;
}
h1,
h2 {
font-weight: 700;
font-size: 1.2em;
}
h2 {
font-size: 1.1em;
}
h3,
h4,
h5 {
font-weight: 500;
}
blockquote {
padding-left: 10px;
border-left: 3px solid $darker-text-color;
color: $darker-text-color;
white-space: normal;
p:last-child {
margin-bottom: 0;
}
}
b,
strong {
font-weight: 700;
}
em,
i {
font-style: italic;
}
i[role=img] {
font-style: normal;
padding-right: 0.25em;
}
sub {
font-size: smaller;
vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
}
ul,
ol {
margin-left: 2em;
p {
margin: 0;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
}
a {
color: $secondary-text-color;
text-decoration: none;
@ -298,28 +204,23 @@
}
}
@mixin focusable {
outline: 0;
background: lighten($ui-base-color, 4%);
&.status.status-direct {
background: mix(lighten($ui-base-color, 4%), $ui-highlight-color, 95%);
&.muted {
background: transparent;
}
}
.detailed-status,
.detailed-status__action-bar {
background: lighten($ui-base-color, 8%);
}
}
.focusable {
&:focus,
&:hover {
@include focusable;
&:focus {
outline: 0;
background: lighten($ui-base-color, 4%);
&.status.status-direct {
background: mix(lighten($ui-base-color, 4%), $ui-highlight-color, 95%);
&.muted {
background: transparent;
}
}
.detailed-status,
.detailed-status__action-bar {
background: lighten($ui-base-color, 8%);
}
}
}
@ -471,10 +372,6 @@
.notification__message {
margin: -10px 0 10px;
}
.reactions-bar--empty {
display: none;
}
}
.notification-favourite {
@ -624,10 +521,6 @@
align-items: center;
display: flex;
margin-top: 8px;
& > .emoji-picker-dropdown > .emoji-button {
padding: 0;
}
}
.status__action-bar-button {
@ -636,10 +529,6 @@
&.icon-button--with-counter {
margin-inline-end: 14px;
}
.fa-plus {
padding-top: 1px;
}
}
.status__action-bar-dropdown {
@ -713,10 +602,6 @@
display: flex;
flex-direction: row;
padding: 10px 0;
.fa-plus {
padding-top: 2px;
}
}
.detailed-status__link {
@ -763,7 +648,6 @@
}
a.status__display-name,
.quote-indicator__display-name,
.reply-indicator__display-name,
.detailed-status__display-name,
.account__display-name {
@ -1157,8 +1041,7 @@ a.status-card.compact:hover {
border-bottom: 0;
.status__content,
.status__action-bar,
.reactions-bar {
.status__action-bar {
margin-inline-start: 46px + 10px;
width: calc(100% - (46px + 10px));
}

View file

@ -14,7 +14,6 @@
.status__content a,
.link-footer a,
.quote-indicator__content a,
.reply-indicator__content a,
.status__content__read-more-button,
.status__content__translate-button {

View file

@ -263,7 +263,6 @@ html {
}
// Change the background colors of status__content__spoiler-link
.quote-indicator__content .status__content__spoiler-link,
.reply-indicator__content .status__content__spoiler-link,
.status__content .status__content__spoiler-link {
background: $ui-base-color;
@ -628,14 +627,12 @@ html {
}
}
.quote-indicator,
.reply-indicator {
background: transparent;
border: 1px solid lighten($ui-base-color, 8%);
}
.status__content,
.quote-indicator__content,
.reply-indicator__content {
a {
color: $highlight-text-color;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,138 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg299"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
sodipodi:docname="treehouse-icon-wordmark.svg"
xml:space="preserve"
inkscape:export-filename="safari-1024x1024.png"
inkscape:export-xdpi="260.10001"
inkscape:export-ydpi="260.10001"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview301"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2.1739703"
inkscape:cx="171.11549"
inkscape:cy="141.21628"
inkscape:window-width="1623"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs296"><linearGradient
id="linearGradient34140"
inkscape:swatch="solid"><stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop34138" /></linearGradient><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath266"><rect
style="fill:#000000;stroke-width:0.75"
id="rect268"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath262"><rect
style="fill:#000000;stroke-width:0.75"
id="rect264"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath258"><rect
style="fill:#000000;stroke-width:0.75"
id="rect260"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath254"><rect
style="fill:#000000;stroke-width:0.75"
id="rect256"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath250"><rect
style="fill:#000000;stroke-width:0.75"
id="rect252"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath246"><rect
style="fill:#000000;stroke-width:0.75"
id="rect248"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><mask
maskUnits="userSpaceOnUse"
id="mask1677"><image
width="100"
height="100"
preserveAspectRatio="none"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAM4AAADOCAYAAAB2Hz3EAAAABHNCSVQICAgIfAhkiAAACLpJREFU eJzt3W2MXFUdx/Hv3rZS2l3a2tqUllZiaMFqAtQUiBIfXqClaEKiMRoFDFgjhJg0xhgVE40afaGS gBHxkQokRmLUGLQqvjBqlIoIAY2lVdSU1kK7LWX73DK+ODPs7HZ2985/7sydu/v9JDf7NHPu/8X+ 8j9799xzB2q1Gn1uKbCifiwHXgEsrB8LgEFgDnAOMKv+npcB83OOP1AfqwhzgbMLGqtoh4ETBY01 ApzM+drjwJGmrw8CL9Y/Hq5/PAAMA7uAvcB/gd1A3/5yDvRRcM4GLgEuA9YBFwFrKO6XWtVyFHgK 2AE8BmyrH8+XWVRDmcEZIAVlA3A1cAWpc0gTqQFPAlvrx+/I3/kKVUZwLgLeD1wHrOr1yTWtDAMP AFuAP/byxL0KTgZcC2wGruzFCTXj/AO4Hfg+cKzbJ+t2cGYDNwAfAy7s5omkur3AHfVjpFsn6WZw NgBfAdZ26wTSJPYAtwH3kK7iFaobwVkJ3AVcU/TAUsCjwE2kK3OFyQoca4BU4BMYGvWPdaTL2J+h wKu2RXWcxaSW+PYiBpO65BHg3cDTnQ5URHAuBn4CnN/pQFIPHATeA/yyk0E6naptAH6PoVF1LAQe BG7tZJBOgvNe4KektWJSlcwC7gQ+Hx0gGpxNwH2kxZRSVX0K+GrkjZHgvI90ubnIK3JSWTYDn273 Te1eHNgI/Bg7jaafW0gNIZd2gvMa4GHy3+ciVclp0r9TtuZ5cd7gLAD+DKyO1yX1vWFgPfCvqV6Y 5++UAeBeDI2mv5cDPyLdyTupPMG5EXhHpxVJFXEJ8NmpXjTVVG0Vae3ZOQUVJVXBadJ9Y3+a6AVT dZy7MTSaeWYB32WSRaGTBedq0pIaaSZ6NXDzRD+caKo2G3gcb0LTzLYfuIC0MHSMiTrODRgaaTHw iVY/aNVxMmA7KWnSTDdCWv2/v/mbrTrOtRgaqWGQFn/rtOo4fwBe34uKpIrYDbwSONX4xviOcyGG RhpvOfDW5m+MD871vatFqpQx2Wieqg0A/yFt7yRprGPAMuqbvjd3nPUYGmkic4GrGl80B2dj72uR KuVtjU+ag3NVixdKGvVSc2n8jTOXNHfzlmhpcquBnY2OczGGRspjPYxO1a4osRCpSi6H0eBcWmIh UpWsg9HgrCmxEKlKVsPoxYFhYFGp5UjVsSgDlmBopHZckAEryq5CqpiVGWnlp6T8zjU4UvuWZaT7 qiXltyTDB0NJ7RrKgKGyq5AqZtCOI7XPjiMF2HGkgKGM9NAoSfkN+gBcqX1ZRo6nT0kaYygDziq7 CqlqnKpJAQZHat/sDJhXdhVSxczPmOQ5h5Jac6omBWSkJ+xKaoNLbqQAp2pSgMGRAgZqtdppDJDU lgxDI7XN0EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUY HCnA4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA 4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA4EgB BkcKMDhSgMGRAgyOFGBwpACDIwVkwOGyi5CqJgNOlV2EVDVO1aQAgyMFGBwpwOBIAQZHCjA4UoDB kQIMjhRgcKQAgyMFGBwpIAOOlF2EVDUZcLLsIqSqcaomBRgcKcDgSAEGRwowOFKAwZECDI4UYHCk AIMjBRgcKcDgSAEGRwowOFKAwZECvK1ACvBGNql9h52qSe07ZXCkgAw4UXYRUtVkwNGyi5AqZsSp mtS+0xlwqOwqpIoZyYAXyq5CqpiRDBgpuwqpYuw4UsALdhypfXYcKeBQBuwvuwqpYoYz4H9lVyFV zJ4M2F12FVLF7MmAXWVXIVXMMwO1Wg3S6oGhkouRqmJxY63azlLLkKrjOeoXBwD+VmYlUoU8BaOb dTxeYiFSlTwGo8F5tMRCpCp5GEaDsw04VV4tUmVsg9HgjAB/Ka8WqRKeZdzfOAC/KacWqTJ+AdRg bHAeLKcWqTK2Nj5p/AMUUoieAZaVUZHU506QsnEAxnacF4H7yqhIqoCfUw8NnLnp+r29rUWqjC3N XzRP1RoeAV7Xs3Kk/rcPWEHT5p2t9lW7vWflSNVwB+N2vG3VcWYD/wRW9agoqZ8dBs4ndZ2XtOo4 p4Av96AgqQq+w7jQQOuOA3AW8HfgVV0uSupnh4DVpBUDY0y0d/Rx4OPdrEiqgC/SIjQwccdp+C3w xm5UJPW5p4G1wLFWP5zqaQUfxMeAaOapAZuYIDQwdXB2ALcVWZFUAXczxaLnqaZqkML1EPCWgoqS +tlO4FKm2Bo6T3AAlpLu1zmv87qkvnUUeAPw16lemPeJbM8C78LnhWp620SO0ED+4EC61/pG0ipq abr5EnB/3hfnnao1uxn4ertvkvrYt4EPUb+7M4/Iw3PvAj4ZeJ/Uj7YAH6aN0ECs4zTcSlo1OhAd QCrZN4FbgNPtvrGTx7V/DbgOONnBGFJZPkfqNG2HBjrrOA1XAj8Ezu10IKkHRkhXz37QySBFBAdS aB4gXQOX+tUO4J3AE50O1MlUrdke4M2k5Tn+r0f9pgZ8g7QlQMehgeI6TrPXAvfgvgXqD/8mTc0e KnLQojpOsyeBy4CbSJ1IKsMIaQa0loJDA93pOM0GgY8Am4El3TyRVHcE+BbwBdJDoLqi28FpmAd8 APgo3o6t7tgH3Ela1XLGHgFF61VwGjLgTcD1pEWjg708uaadk6QdNr9H2tf5eK9O3OvgNJtHuhJ3 DbCRtAWPNJXngF8BPwN+DQyXUUSZwRlvJemiwuXAOmBN/XuaufYB20mPD9xWP7bT5rqybuin4LQy n7Q9z3nA8vqxFFhYPxYBc4AFpGngEGlDxYY5OB0syzHO3K+isWn586T9+w7WjwOkTrIX2AXsJv2z spRuksf/AQGJjDhvlYqLAAAAAElFTkSuQmCC "
id="image1679"
x="-142.08125"
y="-68.527077"
style="fill:#000000;fill-opacity:1" /></mask></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><g
id="g9734"
transform="translate(-0.56747079,-0.60319788)"
style="fill:#000000;fill-opacity:1"><path
id="path9722"
clip-path="url(#clipPath266)"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.04672"
d="m 148.10952,261.04452 c -2.06649,0.0422 -4.13783,0.0165 -6.20654,-0.082 -9.03297,-0.43028 -17.4723,-1.69278 -25.21582,-3.6709 1.95723,-3.04579 3.62861,-6.19047 4.86181,-9.41895 1.0933,-2.86169 2.06819,-6.06832 2.93702,-9.39404 5.97882,1.41185 12.41455,2.26761 19.3374,2.43164 7.8224,0.18531 15.14923,-0.68835 21.97412,-2.31592 0.86115,3.28373 1.82434,6.45027 2.90479,9.27832 1.23038,3.22113 2.88552,6.3635 4.83691,9.40284 -7.87565,2.15196 -16.58364,3.58862 -25.42969,3.76904 z M 86.219871,243.54843 C 79.135999,238.83894 72.903758,233.56337 67.024558,227.68417 49.557998,210.21762 36.413306,184.82735 36.943991,150.03133 37.322903,125.20846 46.732098,105.05671 57.637839,90.085532 61.45103,84.852994 65.425291,79.543181 70.00991,75.366781 c 19.181088,-17.465513 42.57142,-30.269605 77.8667,-30.079102 15.91534,0.08478 31.16013,4.698278 43.30518,10.453125 12.23927,5.799859 22.86841,13.510545 31.35937,22.612794 8.56947,9.187033 15.97962,19.660028 21.33252,32.425782 5.2315,12.47583 9.54041,27.78413 8.74805,45.22559 -0.73166,16.10163 -4.59718,31.20723 -10.45459,43.30517 -8.99238,18.57589 -21.58401,33.19172 -38.1958,44.06397 -2.31466,-6.63911 -4.61328,-13.17798 -6.85254,-19.50586 3.44267,-2.57487 6.59968,-5.30936 9.42187,-8.13135 14.29397,-14.29395 25.48515,-34.26814 26.02735,-61.65234 0.54115,-27.4062 -11.35774,-48.68927 -25.17334,-62.504888 -14.30339,-14.304424 -34.60695,-26.239746 -61.43848,-26.239746 -27.74113,0 -48.035409,10.491046 -62.718751,25.17334 -13.699435,13.699434 -26.239746,34.165224 -26.239746,61.438474 0,27.79034 10.744976,48.07693 25.171875,62.50489 3.314031,3.31403 6.965461,6.49069 10.954102,9.43213 -2.257102,6.37839 -4.571641,12.9692 -6.903809,19.65967 z"
transform="matrix(0.35277777,0,0,-0.35277777,-0.52260784,108.57872)" /><g
id="g9726"
clip-path="url(#clipPath262)"
transform="matrix(0.35277777,0,0,-0.35277777,-0.52260784,108.57872)"
style="fill:#000000;fill-opacity:1"><path
id="path9724"
style="color:#000000;fill:#000000;stroke-width:7.79528;-inkscape-stroke:none;fill-opacity:1"
d="m 75.314109,288.61142 c 0.792777,-2.69314 1.634814,-5.51875 2.904786,-9.4292 3.288523,-10.12592 7.91432,-23.60109 12.613769,-37.04883 8.510354,-24.35287 15.714366,-44.24376 17.293946,-48.61524 l 17.83447,13.95117 c -0.34406,3.20433 -2.77605,25.20128 -8.05224,39.01172 v 0.001 c -2.69383,7.05234 -7.75838,14.01594 -13.73438,20.22656 -7.323043,7.61056 -15.978751,14.09202 -22.936522,18.52002 -2.358994,1.50129 -4.088107,2.38872 -5.923829,3.38233 z" /></g><g
id="g9730"
clip-path="url(#clipPath258)"
transform="matrix(0.35277777,0,0,-0.35277777,-0.52260784,108.57872)"
style="fill:#000000;fill-opacity:1"><path
id="path9728"
style="color:#000000;fill:#000000;stroke-width:7.79528;-inkscape-stroke:none;fill-opacity:1"
d="m 214.98257,288.76376 c -0.13295,-0.0727 -0.19067,-0.0887 -0.32666,-0.16406 -3.98394,-2.20699 -9.28528,-5.56377 -14.80371,-9.75147 -11.03686,-8.37541 -22.86033,-20.19752 -27.5083,-32.36572 v -0.001 c -5.27619,-13.81042 -7.70819,-35.80734 -8.05225,-39.01172 l 17.83301,-13.95117 c 1.5796,4.37153 8.7836,24.2624 17.29394,48.61524 4.69945,13.44774 9.32378,26.92291 12.61231,37.04883 1.29043,3.97344 2.15527,6.86878 2.95166,9.58154 z" /></g><path
d="m 173.4755,165.60476 c -0.4773,6.26252 -0.12456,18.21709 -3.41335,22.39978 -2.90046,3.68864 -10.18248,3.58187 -17.06675,3.83936 -6.86752,0.2575 -12.48631,0.65316 -17.91878,0 -5.70462,-0.6856 -11.77872,-0.0314 -14.93354,-3.41335 -3.47091,-3.72108 -3.30135,-14.73047 -3.83936,-21.97272 -1.10533,-14.86654 -0.58616,-29.35627 -13.86693,-31.14616 0.12247,-1.65486 -0.25749,-3.8132 0.21353,-5.1195 27.39054,-0.17271 56.5605,-0.48045 84.47858,0 0.3203,1.41097 0.3203,3.70852 0,5.1195 -13.07247,1.98981 -12.54074,15.70601 -13.6534,30.29309 m -40.53209,59.51852 h 24.31946 c 0.49301,-3.98695 0.0743,-8.8856 0.21249,-13.22634 12.614,-1.12731 25.68962,-0.0429 31.35969,-7.8933 4.38052,-6.06469 4.12303,-17.83295 4.90702,-28.15988 1.35027,-17.79945 0.3496,-34.36692 18.34584,-35.41154 -0.2397,-9.97523 0.50661,-21.91934 -0.42706,-30.50662 h -54.39798 c -0.62908,-4.70396 0.20307,-10.87018 -0.42706,-15.57308 -8.03671,0.1413 -16.63864,-0.28262 -24.31946,0.21353 -0.49405,4.62545 -0.0733,10.16573 -0.21353,15.14602 -18.17209,0.56523 -38.38841,-0.10153 -54.825041,0.42706 v 30.50662 c 5.375951,0.63012 10.631521,2.43153 13.438821,6.39964 3.98172,5.62506 4.12408,16.23356 4.90702,25.81208 1.21629,14.87806 0.30041,30.53069 10.66607,35.62612 6.43104,3.16109 15.7573,2.71624 25.81313,3.19982 0.75259,3.9409 -0.45951,9.84648 0.64059,13.43987"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.75;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path9732"
clip-path="url(#clipPath254)"
transform="matrix(0.35277777,0,0,-0.35277777,-0.52260784,108.57872)" /></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79" height="79" viewBox="0 0 79 75"><symbol id="logo-symbol-icon"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></symbol><use xlink:href="#logo-symbol-icon"/></svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -44,8 +44,6 @@ const notFoundFn = () => (
set='twitter'
size={32}
sheetSize={32}
sheetColumns={60}
sheetRows={60}
backgroundImageFn={backgroundImageFn}
/>
@ -104,12 +102,12 @@ class ModifierPickerMenu extends PureComponent {
return (
<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} sheetColumns={60} sheetRows={60} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={6} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
</div>
);
}
@ -144,7 +142,7 @@ class ModifierPicker extends PureComponent {
return (
<div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div>
);
@ -281,8 +279,6 @@ class EmojiPickerMenuImpl extends PureComponent {
perLine={8}
emojiSize={22}
sheetSize={32}
sheetColumns={60}
sheetRows={60}
custom={buildCustomEmojis(custom_emojis)}
color=''
emoji=''

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M800-680v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM620-520q25 0 42.5-17.5T680-580q0-25-17.5-42.5T620-640q-25 0-42.5 17.5T560-580q0 25 17.5 42.5T620-520Zm-280 0q25 0 42.5-17.5T400-580q0-25-17.5-42.5T340-640q-25 0-42.5 17.5T280-580q0 25 17.5 42.5T340-520Zm140 260q68 0 123.5-38.5T684-400H276q25 63 80.5 101.5T480-260Zm0 180q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q43 0 83 8.5t77 24.5v167h80v80h142q9 29 13.5 58.5T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>

Before

Width:  |  Height:  |  Size: 622 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-480Zm0 400q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q43 0 83 8.5t77 24.5v90q-35-20-75.5-31.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160q133 0 226.5-93.5T800-480q0-32-6.5-62T776-600h86q9 29 13.5 58.5T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm320-600v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM620-520q25 0 42.5-17.5T680-580q0-25-17.5-42.5T620-640q-25 0-42.5 17.5T560-580q0 25 17.5 42.5T620-520Zm-280 0q25 0 42.5-17.5T400-580q0-25-17.5-42.5T340-640q-25 0-42.5 17.5T280-580q0 25 17.5 42.5T340-520Zm140 260q68 0 123.5-38.5T684-400H276q25 63 80.5 101.5T480-260Z"/></svg>

Before

Width:  |  Height:  |  Size: 744 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92Z"/></svg>

Before

Width:  |  Height:  |  Size: 322 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92ZM320-500q25 0 42.5-17.5T380-560q0-25-17.5-42.5T320-620q-25 0-42.5 17.5T260-560q0 25 17.5 42.5T320-500Zm360 0q25 0 42.5-17.5T740-560q0-25-17.5-42.5T680-620q-25 0-42.5 17.5T620-560q0 25 17.5 42.5T680-500Zm0-60Zm-360 0Z"/></svg>

Before

Width:  |  Height:  |  Size: 538 B

View file

@ -39,8 +39,6 @@ class ActivityPub::Activity
ActivityPub::Activity::Follow
when 'Like'
ActivityPub::Activity::Like
when 'EmojiReact'
ActivityPub::Activity::EmojiReact
when 'Block'
ActivityPub::Activity::Block
when 'Update'
@ -178,32 +176,4 @@ class ActivityPub::Activity
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_actor] && "via #{@options[:relayed_through_actor].uri}"}")
nil
end
# Ensure emoji declared in the activity's tags are
# present in the database and downloaded to the local cache.
# Required by EmojiReact and Like for emoji reactions.
def process_emoji_tags(name, tags)
tag = as_array(tags).find { |item| item['type'] == 'Emoji' }
return if tag.nil?
custom_emoji_parser = ActivityPub::Parser::CustomEmojiParser.new(tag)
return if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank? || !name.eql?(custom_emoji_parser.shortcode)
emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain)
return emoji unless emoji.nil? ||
custom_emoji_parser.image_remote_url != emoji.image_remote_url ||
(custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at)
begin
emoji ||= CustomEmoji.new(domain: @account.domain,
shortcode: custom_emoji_parser.shortcode,
uri: custom_emoji_parser.uri)
emoji.image_remote_url = custom_emoji_parser.image_remote_url
emoji.save
rescue Seahorse::Client::NetworkingError => e
Rails.logger.warn "Error fetching emoji: #{e}"
return
end
emoji
end
end

View file

@ -130,7 +130,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
media_attachment_ids: attachment_ids,
ordered_media_attachment_ids: attachment_ids,
poll: process_poll,
quote: process_quote,
}
end
@ -431,28 +430,4 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
poll.reload
retry
end
def guess_quote_url
if @object["quoteUri"] && !@object["quoteUri"].empty?
@object["quoteUri"]
elsif @object["quoteUrl"] && !@object["quoteUrl"].empty?
@object["quoteUrl"]
elsif @object["quoteURL"] && !@object["quoteURL"].empty?
@object["quoteURL"]
elsif @object["_misskey_quote"] && !@object["_misskey_quote"].empty?
@object["_misskey_quote"]
else
nil
end
end
def process_quote
url = guess_quote_url
return nil if url.nil?
quote = ResolveURLService.new.call(url)
status_from_uri(quote.uri) if quote
rescue
nil
end
end

View file

@ -1,26 +0,0 @@
# frozen_string_literal: true
class ActivityPub::Activity::EmojiReact < ActivityPub::Activity
def perform
original_status = status_from_uri(object_uri)
name = @json['content']
return if original_status.nil? ||
!original_status.account.local? ||
delete_arrived_first?(@json['id'])
if /^:.*:$/.match?(name)
name.delete! ':'
custom_emoji = process_emoji_tags(name, @json['tag'])
return if custom_emoji.nil?
end
return if @account.reacted?(original_status, name, custom_emoji)
reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji)
LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction')
rescue ActiveRecord::RecordInvalid
nil
end
end

View file

@ -3,39 +3,12 @@
class ActivityPub::Activity::Like < ActivityPub::Activity
def perform
original_status = status_from_uri(object_uri)
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id'])
return if maybe_process_embedded_reaction
return if @account.favourited?(original_status)
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status)
favourite = original_status.favourites.create!(account: @account)
LocalNotificationWorker.perform_async(original_status.account_id, favourite.id, 'Favourite', 'favourite')
Trends.statuses.register(original_status)
end
# Some servers deliver reactions as likes with the emoji in content
# Versions of Misskey before 12.1.0 specify emojis in _misskey_reaction instead, so we check both
# See https://misskey-hub.net/ns.html#misskey-reaction for details
def maybe_process_embedded_reaction
original_status = status_from_uri(object_uri)
name = @json['content'] || @json['_misskey_reaction']
return false if name.nil?
if /^:.*:$/.match?(name)
name.delete! ':'
custom_emoji = process_emoji_tags(name, @json['tag'])
return false if custom_emoji.nil? # invalid custom emoji, treat it as a regular like
end
return true if @account.reacted?(original_status, name, custom_emoji)
reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji)
LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction')
true
# account tried to react with disabled custom emoji. Returning true to discard activity.
rescue ActiveRecord::RecordInvalid
true
end
end

View file

@ -11,8 +11,6 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
undo_follow
when 'Like'
undo_like
when 'EmojiReact'
undo_emoji_react
when 'Block'
undo_block
when nil
@ -110,31 +108,6 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
if @account.favourited?(status)
favourite = status.favourites.where(account: @account).first
favourite&.destroy
elsif @object['content'].present? || @object['_misskey_reaction'].present?
undo_emoji_react
else
delete_later!(object_uri)
end
end
def undo_emoji_react
name = @object['content'] || @object['_misskey_reaction']
return if name.nil?
status = status_from_uri(target_uri)
return if status.nil? || !status.account.local?
if /^:.*:$/.match?(name)
name.delete! ':'
custom_emoji = process_emoji_tags(name, @object['tag'])
return if custom_emoji.nil?
end
if @account.reacted?(status, name, custom_emoji)
reaction = status.status_reactions.where(account: @account, name: name).first
reaction&.destroy
else
delete_later!(object_uri)
end

View file

@ -83,15 +83,11 @@ class ActivityPub::TagManager
# Unlisted and private statuses go out primarily to the followers collection
# Others go out only to the people they mention
def to(status)
to = []
to << uri_for(status.quote.account) if status.quote?
case status.visibility
when 'public'
to << COLLECTIONS[:public]
[COLLECTIONS[:public]]
when 'unlisted', 'private'
to << account_followers_url(status.account)
[account_followers_url(status.account)]
when 'direct', 'limited'
if status.account.silenced?
# Only notify followers if the account is locally silenced

View file

@ -6,8 +6,8 @@ class NotificationMailer < ApplicationMailer
:routing
before_action :process_params
before_action :set_status, only: [:mention, :favourite, :reaction, :reblog]
before_action :set_account, only: [:follow, :favourite, :reaction, :reblog, :follow_request]
before_action :set_status, only: [:mention, :favourite, :reblog]
before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request]
after_action :set_list_headers!
default to: -> { email_address_with_name(@user.email, @me.username) }
@ -40,15 +40,6 @@ class NotificationMailer < ApplicationMailer
end
end
def reaction
return unless @user.functional? && @status.present?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail subject: default_i18n_subject(name: @account.acct)
end
end
def reblog
return unless @user.functional? && @status.present?

View file

@ -4,6 +4,7 @@
#
# Table name: accounts
#
# id :bigint(8) not null, primary key
# username :string default(""), not null
# domain :string
# private_key :text
@ -16,11 +17,11 @@
# url :string
# avatar_file_name :string
# avatar_content_type :string
# avatar_file_size :bigint(8)
# avatar_file_size :integer
# avatar_updated_at :datetime
# header_file_name :string
# header_content_type :string
# header_file_size :bigint(8)
# header_file_size :integer
# header_updated_at :datetime
# avatar_remote_url :string
# locked :boolean default(FALSE), not null
@ -31,7 +32,6 @@
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# id :bigint(8) not null, primary key
# memorial :boolean default(FALSE), not null
# moved_to_account_id :bigint(8)
# featured_collection_url :string
@ -45,8 +45,8 @@
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
# devices_url :string
# sensitized_at :datetime
# suspension_origin :integer
# sensitized_at :datetime
# trendable :boolean
# reviewed_at :datetime
# requested_review_at :datetime

View file

@ -13,7 +13,6 @@ module Account::Associations
# Timelines
has_many :statuses, inverse_of: :account, dependent: :destroy
has_many :favourites, inverse_of: :account, dependent: :destroy
has_many :status_reactions, inverse_of: :account, dependent: :destroy
has_many :bookmarks, inverse_of: :account, dependent: :destroy
has_many :mentions, inverse_of: :account, dependent: :destroy
has_many :notifications, inverse_of: :account, dependent: :destroy

View file

@ -230,10 +230,6 @@ module Account::Interactions
status.proper.favourites.exists?(account: self)
end
def reacted?(status, name, custom_emoji = nil)
status.proper.status_reactions.exists?(account: self, name: name, custom_emoji: custom_emoji)
end
def bookmarked?(status)
status.proper.bookmarks.exists?(account: self)
end

View file

@ -123,10 +123,6 @@ module User::HasSettings
settings['hide_followers_count']
end
def setting_visible_reactions
integer_cast_setting('visible_reactions', 0)
end
def allows_report_emails?
settings['notification_emails.report']
end
@ -170,14 +166,4 @@ module User::HasSettings
def hide_all_media?
settings['web.display_media'] == 'hide_all'
end
def integer_cast_setting(key, min = nil, max = nil)
i = ActiveModel::Type::Integer.new.cast(settings[key])
# the cast above doesn't return a number if passed the string "e"
i = 0 unless i.is_a? Numeric
return min if !min.nil? && i < min
return max if !max.nil? && i > max
i
end
end

View file

@ -9,7 +9,7 @@
# domain :string
# image_file_name :string
# image_content_type :string
# image_file_size :bigint(8)
# image_file_size :integer
# image_updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null

View file

@ -4,16 +4,16 @@
#
# Table name: imports
#
# id :bigint(8) not null, primary key
# type :integer not null
# approved :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# data_file_name :string
# data_content_type :string
# data_file_size :bigint(8)
# data_file_size :integer
# data_updated_at :datetime
# account_id :bigint(8) not null
# id :bigint(8) not null, primary key
# overwrite :boolean default(FALSE), not null
#

View file

@ -19,11 +19,6 @@
class Invite < ApplicationRecord
include Expireable
# FIXME: make this a rails cfg key or whatev?
TH_USE_INVITE_QUOTA = !!ENV['TH_USE_INVITE_QUOTA']
TH_INVITE_MAX_USES = ENV.fetch('TH_INVITE_MAX_USES', 25).to_i
TH_ACTIVE_INVITE_SLOT_QUOTA = ENV.fetch('TH_ACTIVE_INVITE_SLOT_QUOTA', 40).to_i
belongs_to :user, inverse_of: :invites
has_many :users, inverse_of: :invite, dependent: nil
@ -31,15 +26,6 @@ class Invite < ApplicationRecord
validates :comment, length: { maximum: 420 }
with_options if: :th_use_invite_quota?, unless: :created_by_moderator? do |invite|
invite.validates :expires_at, presence: true
invite.validate :expires_in_at_most_one_week?
invite.validates :max_uses, presence: true
# In Rails 6.1, numericality doesn't support :in
invite.validates :max_uses, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: TH_INVITE_MAX_USES }
invite.validate :reasonable_outstanding_invite_count?
end
before_validation :set_code
def valid_for_use?
@ -54,31 +40,4 @@ class Invite < ApplicationRecord
break if Invite.find_by(code: code).nil?
end
end
def created_by_moderator?
self.user.moderator
end
def th_use_invite_quota?
TH_USE_INVITE_QUOTA
end
def expires_in_at_most_one_week?
return if self.expires_in.to_i.seconds <= 1.week
# FIXME: Localize this
errors.add(:expires_in, 'must expire within one week')
end
def reasonable_outstanding_invite_count?
valid_invites = self.user.invites.filter { |i| i.valid_for_use? }
count = valid_invites.sum do |i|
next i.max_uses unless i.max_uses.nil?
errors.add(:max_uses, 'must not have any active unlimited-use invites')
return
end
return if count + max_uses <= TH_ACTIVE_INVITE_SLOT_QUOTA
errors.add(:max_uses, "must not exceed active invite slot quota of #{TH_ACTIVE_INVITE_SLOT_QUOTA}")
end
end

View file

@ -4,10 +4,11 @@
#
# Table name: media_attachments
#
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# file_file_name :string
# file_content_type :string
# file_file_size :bigint(8)
# file_file_size :integer
# file_updated_at :datetime
# remote_url :string default(""), not null
# created_at :datetime not null
@ -16,7 +17,6 @@
# type :integer default("image"), not null
# file_meta :json
# account_id :bigint(8)
# id :bigint(8) not null, primary key
# description :text
# scheduled_status_id :bigint(8)
# blurhash :string
@ -24,7 +24,7 @@
# file_storage_schema_version :integer
# thumbnail_file_name :string
# thumbnail_content_type :string
# thumbnail_file_size :bigint(8)
# thumbnail_file_size :integer
# thumbnail_updated_at :datetime
# thumbnail_remote_url :string
#

Some files were not shown because too many files have changed in this diff Show more