th: deal with "that one spammer" #58
|
@ -1,6 +1,5 @@
|
|||
[production]
|
||||
defaults
|
||||
not IE 11
|
||||
not dead
|
||||
|
||||
[development]
|
||||
|
|
|
@ -1,21 +1,39 @@
|
|||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Order-independent
|
||||
*.sw*
|
||||
*~
|
||||
.DS_Store
|
||||
.bundle
|
||||
.env
|
||||
.env.*
|
||||
.git
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.github
|
||||
public/system
|
||||
public/assets
|
||||
public/packs
|
||||
node_modules
|
||||
.gitignore
|
||||
.woodpecker.yml
|
||||
/*.md
|
||||
build
|
||||
chart
|
||||
coverage
|
||||
data
|
||||
elasticsearch
|
||||
log
|
||||
neo4j
|
||||
vendor/bundle
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
||||
node_modules
|
||||
postgres
|
||||
postgres14
|
||||
public/assets
|
||||
public/packs
|
||||
public/packs-test
|
||||
public/system
|
||||
redis
|
||||
elasticsearch
|
||||
chart
|
||||
sorbet
|
||||
tmp
|
||||
vendor/bundle
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
LOCAL_DOMAIN=localhost
|
||||
ALTERNATE_DOMAINS=mastodon.internal
|
||||
|
||||
DB_HOST=$(pwd)/data/postgres
|
||||
DB_USER=mastodon
|
||||
DB_NAME=mastodon_dev
|
||||
REDIS_URL=unix://./data/redis/redis-dev.sock
|
|
@ -14,7 +14,7 @@
|
|||
# ----------
|
||||
# This identifies your server and cannot be changed safely later
|
||||
# ----------
|
||||
LOCAL_DOMAIN=example.com
|
||||
LOCAL_DOMAIN=localhost
|
||||
|
||||
# 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,6 +25,7 @@ LOCAL_DOMAIN=example.com
|
|||
# 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
|
||||
|
@ -43,14 +44,14 @@ LOCAL_DOMAIN=example.com
|
|||
|
||||
# Redis
|
||||
# -----
|
||||
REDIS_HOST=localhost
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
|
||||
# PostgreSQL
|
||||
# ----------
|
||||
DB_HOST=/var/run/postgresql
|
||||
DB_USER=mastodon
|
||||
DB_HOST=db
|
||||
DB_USER=postgres
|
||||
DB_NAME=mastodon_production
|
||||
DB_PASS=
|
||||
DB_PORT=5432
|
||||
|
|
|
@ -3,3 +3,8 @@ NODE_ENV=tests
|
|||
# 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
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
# 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
|
||||
|
@ -12,6 +15,9 @@
|
|||
/db/*.sqlite3
|
||||
/db/*.sqlite3-journal
|
||||
|
||||
# Ignore local data directory
|
||||
/data
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
.eslintcache
|
||||
/log/*
|
||||
|
@ -63,3 +69,12 @@ 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
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
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
|
||||
|
||||
# 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'
|
||||
|
||||
build:
|
||||
<<: *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 tag $NAME:latest $NAME:$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:%
|
||||
when:
|
||||
event: [push, tag]
|
||||
branch: main
|
||||
secrets: [REGISTRY_SECRET]
|
||||
environment:
|
||||
<<: *docker-environment
|
||||
REGISTRY_USER: ariadne
|
|
@ -0,0 +1,5 @@
|
|||
enableGlobalCache: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
|
@ -1,10 +1,10 @@
|
|||
# Contributing to Mastodon Glitch Edition
|
||||
# Contributing to Mastodon Glitch+Treehouse Edition #
|
||||
|
||||
Thank you for your interest in contributing to the `glitch-soc` project!
|
||||
Thank you for your interest in contributing to the **Treehouse Mastodon** 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!)
|
||||
> (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!)
|
||||
|
||||
## Translations
|
||||
|
||||
|
@ -12,26 +12,31 @@ You can submit glitch-soc-specific translations via [Crowdin](https://crowdin.co
|
|||
|
||||
[![Crowdin](https://badges.crowdin.net/glitch-soc/localized.svg)](https://crowdin.com/project/glitch-soc)
|
||||
|
||||
## Planning
|
||||
## 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
|
||||
## Documentation ##
|
||||
|
||||
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.
|
||||
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/)).
|
||||
|
||||
## Frontend Development
|
||||
## 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).
|
||||
|
||||
## Frontend Development ##
|
||||
|
||||
Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information.
|
||||
|
||||
## Backend Development
|
||||
## Backend Development ##
|
||||
|
||||
See the guidelines below.
|
||||
|
||||
---
|
||||
- - -
|
||||
|
||||
You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `mastodon/mastodon`, reproduced below.
|
||||
|
||||
|
@ -80,6 +85,8 @@ It is not always possible to phrase every change in such a manner, but it is des
|
|||
- Code style rules (rubocop, eslint)
|
||||
- Normalization of locale files (i18n-tasks)
|
||||
|
||||
**Note**: You may need to log in and authorise the GitHub account your fork of this repository belongs to with CircleCI to enable some of the automated checks to run.
|
||||
|
||||
## Documentation
|
||||
|
||||
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation).
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Divergences
|
||||
|
||||
## Major Features
|
||||
|
||||
- quote posting
|
||||
- TreehouseAutomodExt (experimental feature flagged)
|
||||
|
||||
## Other Changes
|
||||
|
||||
- various build system changes
|
||||
- a better dockerfile
|
||||
- yarn v2 (a mistake, tbh)
|
||||
- various dev env changes
|
||||
- various css/style changes
|
47
Dockerfile
|
@ -1,23 +1,24 @@
|
|||
# syntax=docker/dockerfile:1.4
|
||||
# This needs to be bullseye-slim because the Ruby image is built on bullseye-slim
|
||||
ARG NODE_VERSION="16.20-bullseye-slim"
|
||||
ARG NODE_VERSION="18.15-bullseye-slim"
|
||||
|
||||
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby
|
||||
FROM node:${NODE_VERSION} as build
|
||||
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.1-slim as ruby
|
||||
FROM node:${NODE_VERSION} as build-base
|
||||
|
||||
COPY --link --from=ruby /opt/ruby /opt/ruby
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive" \
|
||||
PATH="${PATH}:/opt/ruby/bin"
|
||||
PATH="${PATH}:/opt/ruby/bin" \
|
||||
NODE_OPTIONS=--openssl-legacy-provider
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
WORKDIR /opt/mastodon
|
||||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends build-essential \
|
||||
ca-certificates \
|
||||
git \
|
||||
libicu-dev \
|
||||
libidn11-dev \
|
||||
|
@ -31,14 +32,36 @@ RUN apt-get update && \
|
|||
ca-certificates \
|
||||
libreadline8 \
|
||||
python3 \
|
||||
shared-mime-info && \
|
||||
shared-mime-info
|
||||
|
||||
COPY --link .yarn/releases/ /opt/mastodon/.yarn/releases/
|
||||
COPY --link Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
||||
|
||||
RUN \
|
||||
bundle config set --local deployment 'true' && \
|
||||
bundle config set --local without 'development test' && \
|
||||
bundle config set silence_root_warning true && \
|
||||
bundle install -j"$(nproc)" && \
|
||||
yarn install --pure-lockfile --production --network-timeout 600000 && \
|
||||
yarn install --immutable && \
|
||||
yarn cache clean
|
||||
|
||||
# Precompile assets
|
||||
# TODO(kouhai): we're currently patching node_modules because of emoji-mart.
|
||||
# we should integrate our own fork instead.
|
||||
COPY --link . /opt/mastodon
|
||||
|
||||
FROM build-base AS build
|
||||
|
||||
ENV RAILS_ENV="production" \
|
||||
NODE_ENV="production"
|
||||
|
||||
ENV OTP_SECRET=precompile_placeholder \
|
||||
SECRET_KEY_BASE=precompile_placeholder \
|
||||
RAKE_NO_YARN_INSTALL_HACK=1
|
||||
RUN mv ./emoji_data/all.json ./node_modules/emoji-mart/data/all.json && \
|
||||
bundle exec rails assets:precompile
|
||||
|
||||
|
||||
FROM node:${NODE_VERSION}
|
||||
|
||||
ARG UID="991"
|
||||
|
@ -78,21 +101,19 @@ RUN apt-get update && \
|
|||
# Note: no, cleaning here since Debian does this automatically
|
||||
# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem
|
||||
|
||||
COPY --chown=mastodon:mastodon . /opt/mastodon
|
||||
COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
|
||||
COPY --link --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
|
||||
|
||||
ARG SOURCE_TAG=''
|
||||
ENV RAILS_ENV="production" \
|
||||
NODE_ENV="production" \
|
||||
RAILS_SERVE_STATIC_FILES="true" \
|
||||
BIND="0.0.0.0"
|
||||
BIND="0.0.0.0" \
|
||||
SOURCE_TAG="${SOURCE_TAG}"
|
||||
|
||||
# Set the run user
|
||||
USER mastodon
|
||||
WORKDIR /opt/mastodon
|
||||
|
||||
# Precompile assets
|
||||
RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile
|
||||
|
||||
# Set the work dir and the container entry point
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
EXPOSE 3000 4000
|
||||
|
|
2
Gemfile
|
@ -147,6 +147,8 @@ group :development do
|
|||
gem 'capistrano-yarn', '~> 2.0'
|
||||
|
||||
gem 'stackprof'
|
||||
|
||||
gem 'foreman'
|
||||
end
|
||||
|
||||
group :production do
|
||||
|
|
|
@ -286,6 +286,7 @@ GEM
|
|||
fog-core (>= 1.45, <= 2.1.0)
|
||||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
foreman (0.87.2)
|
||||
formatador (0.3.0)
|
||||
fugit (1.8.1)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
|
@ -804,6 +805,7 @@ DEPENDENCIES
|
|||
fastimage
|
||||
fog-core (<= 2.4.0)
|
||||
fog-openstack (~> 0.3)
|
||||
foreman
|
||||
fuubar (~> 2.5)
|
||||
haml-rails (~> 2.0)
|
||||
haml_lint
|
||||
|
@ -895,3 +897,9 @@ DEPENDENCIES
|
|||
webpacker (~> 5.4)
|
||||
webpush!
|
||||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.0.5p211
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.6
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
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 run start
|
||||
webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0
|
||||
webpack: env RAILS_ENV=development NODE_ENV=development ./bin/webpack-dev-server --listen-host 0.0.0.0
|
||||
|
|
24
README.md
|
@ -1,14 +1,20 @@
|
|||
# Mastodon Glitch Edition
|
||||
# Mastodon Glitch+Treehouse Edition #
|
||||
|
||||
> Now with automated deploys!
|
||||
> Now with bunny ears!
|
||||
|
||||
[![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]
|
||||
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?
|
||||
|
||||
[circleci]: https://circleci.com/gh/glitch-soc/mastodon
|
||||
[code_climate]: https://codeclimate.com/github/glitch-soc/mastodon
|
||||
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.
|
||||
|
||||
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?
|
||||
## Links
|
||||
|
||||
- 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/).
|
||||
- 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)
|
||||
|
|
9
Rakefile
|
@ -4,3 +4,12 @@
|
|||
require File.expand_path('../config/application', __FILE__)
|
||||
|
||||
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
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
# 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.
|
|
@ -66,7 +66,8 @@ 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
|
||||
with_rate_limit: true,
|
||||
quote_id: status_params[:quote_id].presence
|
||||
)
|
||||
|
||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||
|
@ -137,6 +138,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
:visibility,
|
||||
:language,
|
||||
:scheduled_at,
|
||||
:quote_id,
|
||||
:content_type,
|
||||
allowed_mentions: [],
|
||||
media_ids: [],
|
||||
|
|
|
@ -24,6 +24,7 @@ module ContextHelper
|
|||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, '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
|
||||
|
|
|
@ -15,7 +15,17 @@ module FormattingHelper
|
|||
module_function :extract_status_plain_text
|
||||
|
||||
def status_content_format(status)
|
||||
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)
|
||||
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
|
||||
end
|
||||
|
||||
def rss_status_content_format(status)
|
||||
|
|
|
@ -82,6 +82,9 @@ export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
|||
|
||||
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||
|
||||
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.' },
|
||||
|
@ -135,6 +138,25 @@ 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,
|
||||
|
@ -208,6 +230,7 @@ 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),
|
||||
|
|
|
@ -74,6 +74,8 @@ 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');
|
||||
} else {
|
||||
const spoilerText = normalStatus.spoiler_text || '';
|
||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||
|
@ -83,6 +85,35 @@ 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);
|
||||
|
||||
const quote_account_emojiMap = makeEmojiMap(status.quote.account);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return normalStatus;
|
||||
|
|
|
@ -68,6 +68,7 @@ class Status extends ImmutablePureComponent {
|
|||
status: ImmutablePropTypes.map,
|
||||
account: ImmutablePropTypes.map,
|
||||
onReply: PropTypes.func,
|
||||
onQuote: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onBookmark: PropTypes.func,
|
||||
|
@ -701,7 +702,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('card') && settings.get('inline_preview_cards') && !this.props.muted) {
|
||||
} else if (!status.get('quote') && status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
|
||||
media.push(
|
||||
<Card
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
|
|
|
@ -25,6 +25,7 @@ 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: 'Favourite' },
|
||||
|
@ -58,6 +59,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
onReply: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onQuote: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onDirect: PropTypes.func,
|
||||
onMention: PropTypes.func,
|
||||
|
@ -124,6 +126,17 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
handleQuoteClick = () => {
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
this.props.onQuote(this.props.status, this.context.router.history);
|
||||
} else {
|
||||
// TODO(ariadne): Add an interaction modal for quoting specifically.
|
||||
this.props.onInteractionModal('reply', this.props.status);
|
||||
}
|
||||
}
|
||||
|
||||
handleBookmarkClick = (e) => {
|
||||
this.props.onBookmark(this.props.status, e);
|
||||
};
|
||||
|
@ -310,6 +323,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
obfuscateCount
|
||||
/>
|
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||
<IconButton className='status__action-bar-button' disabled={!publicStatus} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} />
|
||||
|
||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||
{shareButton}
|
||||
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||
|
|
|
@ -336,6 +336,37 @@ class StatusContent extends React.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 class="status__quote">
|
||||
<blockquote>
|
||||
<bdi>
|
||||
<span class="quote-display-name">
|
||||
<Icon
|
||||
fixedWidth
|
||||
id='quote-right'
|
||||
aria-hidden='true'
|
||||
key='icon-quote-right' />
|
||||
<strong class="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 = '';
|
||||
|
||||
|
@ -401,6 +432,7 @@ class StatusContent extends React.PureComponent {
|
|||
{mentionsPlaceholder}
|
||||
|
||||
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
|
||||
{quote}
|
||||
<div
|
||||
ref={this.setContentsRef}
|
||||
key={`contents-${tagLinks}`}
|
||||
|
@ -426,6 +458,7 @@ class StatusContent extends React.PureComponent {
|
|||
onMouseUp={this.handleMouseUp}
|
||||
tabIndex={0}
|
||||
>
|
||||
{quote}
|
||||
<div
|
||||
ref={this.setContentsRef}
|
||||
key={`contents-${tagLinks}-${rewriteMentions}`}
|
||||
|
@ -447,6 +480,7 @@ class StatusContent extends React.PureComponent {
|
|||
className='status__content'
|
||||
tabIndex={0}
|
||||
>
|
||||
{quote}
|
||||
<div
|
||||
ref={this.setContentsRef}
|
||||
key={`contents-${tagLinks}`}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { List as ImmutableList } from 'immutable';
|
|||
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
|
||||
import {
|
||||
replyCompose,
|
||||
quoteCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
|
@ -54,6 +55,8 @@ 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}}' },
|
||||
|
@ -112,6 +115,23 @@ 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('CONFIRM', {
|
||||
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));
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import QuoteIndicatorContainer from '../containers/quote_indicator_container';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
@ -313,6 +314,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
<WarningContainer />
|
||||
|
||||
<ReplyIndicatorContainer />
|
||||
<QuoteIndicatorContainer />
|
||||
|
||||
<div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
|
||||
<AutosuggestInput
|
||||
|
|
|
@ -39,6 +39,8 @@ const notFoundFn = () => (
|
|||
set='twitter'
|
||||
size={32}
|
||||
sheetSize={32}
|
||||
sheetColumns={60}
|
||||
sheetRows={60}
|
||||
backgroundImageFn={backgroundImageFn}
|
||||
/>
|
||||
|
||||
|
@ -97,12 +99,12 @@ class ModifierPickerMenu extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
||||
<button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<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 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 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 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 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 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -137,7 +139,7 @@ class ModifierPicker extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown__modifiers'>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
|
||||
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||
</div>
|
||||
);
|
||||
|
@ -274,6 +276,8 @@ class EmojiPickerMenuImpl extends React.PureComponent {
|
|||
perLine={8}
|
||||
emojiSize={22}
|
||||
sheetSize={32}
|
||||
sheetColumns={60}
|
||||
sheetRows={60}
|
||||
custom={buildCustomEmojis(custom_emojis)}
|
||||
color=''
|
||||
emoji=''
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
// Components.
|
||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
|
||||
// Messages.
|
||||
const messages = defineMessages({
|
||||
cancel: {
|
||||
defaultMessage: 'Cancel',
|
||||
id: 'quote_indicator.cancel',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
class QuoteIndicator extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { onCancel } = this.props;
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = status.get('account');
|
||||
const content = status.get('content');
|
||||
const attachments = status.get('media_attachments');
|
||||
|
||||
// The result.
|
||||
return (
|
||||
<article className='quote-indicator'>
|
||||
<header className='quote-indicator__header'>
|
||||
<IconButton
|
||||
className='quote-indicator__cancel'
|
||||
icon='times'
|
||||
onClick={this.handleClick}
|
||||
title={intl.formatMessage(messages.cancel)}
|
||||
inverted
|
||||
/>
|
||||
<Icon
|
||||
className='quote-indicator__cancel icon-button inverted'
|
||||
id='quote-right' />
|
||||
{account && (
|
||||
<AccountContainer
|
||||
id={account}
|
||||
small
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<div
|
||||
className='quote-indicator__content icon-button translate'
|
||||
dangerouslySetInnerHTML={{ __html: content || '' }}
|
||||
/>
|
||||
{attachments.size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={attachments}
|
||||
/>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(QuoteIndicator)
|
|
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
|
||||
// Components.
|
||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
|
||||
|
@ -57,6 +58,9 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
title={intl.formatMessage(messages.cancel)}
|
||||
inverted
|
||||
/>
|
||||
<Icon
|
||||
className='quote-indicator__cancel icon-button inverted'
|
||||
id='reply' />
|
||||
{account && (
|
||||
<AccountContainer
|
||||
id={account}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { cancelQuoteCompose } from 'flavours/glitch/actions/compose';
|
||||
import QuoteIndicator from '../components/quote_indicator';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const mapStateToProps = state => {
|
||||
const statusId = state.getIn(['compose', 'quote_id']);
|
||||
const editing = false;
|
||||
|
||||
return {
|
||||
status: state.getIn(['statuses', statusId]),
|
||||
editing,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onCancel () {
|
||||
dispatch(cancelQuoteCompose());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(QuoteIndicator);
|
|
@ -18,6 +18,7 @@ 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: 'Favourite' },
|
||||
|
@ -52,6 +53,7 @@ class ActionBar extends React.PureComponent {
|
|||
onReblog: PropTypes.func.isRequired,
|
||||
onFavourite: PropTypes.func.isRequired,
|
||||
onBookmark: PropTypes.func.isRequired,
|
||||
onQuote: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func,
|
||||
onMuteConversation: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
|
@ -81,6 +83,10 @@ class ActionBar extends React.PureComponent {
|
|||
this.props.onBookmark(this.props.status, e);
|
||||
};
|
||||
|
||||
handleQuoteClick = () => {
|
||||
this.props.onQuote(this.props.status);
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history);
|
||||
};
|
||||
|
@ -215,6 +221,7 @@ class ActionBar extends React.PureComponent {
|
|||
<div className='detailed-status__action-bar'>
|
||||
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className='quote-right-icon' disabled={!publicStatus} title={intl.formatMessage(messages.quote)} icon='quote-right' 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' onClick={this.handleFavouriteClick} /></div>
|
||||
{shareButton}
|
||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
||||
|
|
|
@ -221,7 +221,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
);
|
||||
mediaIcons.push('picture-o');
|
||||
}
|
||||
} else if (status.get('card')) {
|
||||
} else if (!status.get('quote') && status.get('card')) {
|
||||
media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />);
|
||||
mediaIcons.push('link');
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import DetailedStatus from '../components/detailed_status';
|
|||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
import {
|
||||
replyCompose,
|
||||
quoteCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
|
@ -33,6 +34,8 @@ import { showAlertForError } from 'flavours/glitch/actions/alerts';
|
|||
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? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
|
@ -68,6 +71,21 @@ 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));
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from 'flavours/glitch/actions/interactions';
|
||||
import {
|
||||
replyCompose,
|
||||
quoteCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
|
@ -327,6 +328,21 @@ 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;
|
||||
|
||||
|
@ -693,6 +709,7 @@ class Status extends ImmutablePureComponent {
|
|||
onFavourite={this.handleFavouriteClick}
|
||||
onReblog={this.handleReblogClick}
|
||||
onBookmark={this.handleBookmarkClick}
|
||||
onQuote={this.handleQuoteClick}
|
||||
onDelete={this.handleDeleteClick}
|
||||
onEdit={this.handleEditClick}
|
||||
onDirect={this.handleDirectClick}
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
COMPOSE_REPLY,
|
||||
COMPOSE_REPLY_CANCEL,
|
||||
COMPOSE_DIRECT,
|
||||
COMPOSE_QUOTE,
|
||||
COMPOSE_QUOTE_CANCEL,
|
||||
COMPOSE_MENTION,
|
||||
COMPOSE_SUBMIT_REQUEST,
|
||||
COMPOSE_SUBMIT_SUCCESS,
|
||||
|
@ -85,6 +87,7 @@ const initialState = ImmutableMap({
|
|||
caretPosition: null,
|
||||
preselectDate: null,
|
||||
in_reply_to: null,
|
||||
quote_id: null,
|
||||
is_submitting: false,
|
||||
is_uploading: false,
|
||||
is_changing_upload: false,
|
||||
|
@ -173,6 +176,7 @@ 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')),
|
||||
|
@ -361,6 +365,51 @@ 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:
|
||||
|
@ -410,45 +459,16 @@ export default function compose(state = initialState, action) {
|
|||
return state
|
||||
.set('elefriend', (state.get('elefriend') + 1) % totalElefriends);
|
||||
case COMPOSE_REPLY:
|
||||
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_QUOTE:
|
||||
return updateWithReply(state, action);
|
||||
case COMPOSE_REPLY_CANCEL:
|
||||
state = state.setIn(['advanced_options', 'threaded_mode'], false);
|
||||
// fall through
|
||||
case COMPOSE_QUOTE_CANCEL:
|
||||
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);
|
||||
|
@ -501,6 +521,7 @@ export default function compose(state = initialState, action) {
|
|||
return item;
|
||||
}));
|
||||
case INIT_MEDIA_EDIT_MODAL:
|
||||
{
|
||||
const media = state.get('media_attachments').find(item => item.get('id') === action.id);
|
||||
return state.set('media_modal', ImmutableMap({
|
||||
id: action.id,
|
||||
|
@ -509,6 +530,7 @@ export default function compose(state = initialState, action) {
|
|||
focusY: media.getIn(['meta', 'focus', 'y'], 0),
|
||||
dirty: false,
|
||||
}));
|
||||
}
|
||||
case COMPOSE_CHANGE_MEDIA_DESCRIPTION:
|
||||
return state.setIn(['media_modal', 'description'], action.description).setIn(['media_modal', 'dirty'], true);
|
||||
case COMPOSE_CHANGE_MEDIA_FOCUS:
|
||||
|
@ -564,6 +586,7 @@ export default function compose(state = initialState, action) {
|
|||
case COMPOSE_DOODLE_SET:
|
||||
return state.mergeIn(['doodle'], action.options);
|
||||
case REDRAFT:
|
||||
{
|
||||
const do_not_federate = !!action.status.get('local_only');
|
||||
let text = action.raw_text || unescapeHTML(expandMentions(action.status));
|
||||
if (do_not_federate) text = text.replace(/ ?👁\ufe0f?\u200b?$/, '');
|
||||
|
@ -604,6 +627,7 @@ export default function compose(state = initialState, action) {
|
|||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
case COMPOSE_SET_STATUS:
|
||||
return state.withMutations(map => {
|
||||
map.set('id', action.status.get('id'));
|
||||
|
|
|
@ -128,6 +128,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.quote-indicator,
|
||||
.reply-indicator {
|
||||
margin: 0 0 10px;
|
||||
border-radius: 4px;
|
||||
|
@ -138,6 +139,7 @@
|
|||
flex: 0 2 auto;
|
||||
}
|
||||
|
||||
.quote-indicator__header,
|
||||
.reply-indicator__header {
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
|
@ -147,11 +149,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.quote-indicator__cancel,
|
||||
.reply-indicator__cancel {
|
||||
float: right;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.quote-indicator__content,
|
||||
.reply-indicator__content {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
|
|
|
@ -68,7 +68,8 @@
|
|||
}
|
||||
|
||||
p,
|
||||
pre {
|
||||
pre,
|
||||
blockquote {
|
||||
margin-bottom: 20px;
|
||||
white-space: pre-wrap;
|
||||
unicode-bidi: plaintext;
|
||||
|
@ -78,6 +79,99 @@
|
|||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
@ -212,23 +306,28 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin focusable {
|
||||
outline: 0;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
|
||||
&.status.status-direct {
|
||||
background: lighten($ui-base-color, 12%);
|
||||
|
||||
&.muted {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-status,
|
||||
.detailed-status__action-bar {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
.focusable {
|
||||
&:focus {
|
||||
outline: 0;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
|
||||
&.status.status-direct {
|
||||
background: lighten($ui-base-color, 12%);
|
||||
|
||||
&.muted {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-status,
|
||||
.detailed-status__action-bar {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
}
|
||||
&:focus,
|
||||
&:hover {
|
||||
@include focusable;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -637,6 +736,7 @@
|
|||
}
|
||||
|
||||
a.status__display-name,
|
||||
.quote-indicator__display-name,
|
||||
.reply-indicator__display-name,
|
||||
.detailed-status__display-name,
|
||||
.account__display-name {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
.status__content a,
|
||||
.link-footer a,
|
||||
.quote-indicator__content a,
|
||||
.reply-indicator__content a,
|
||||
.status__content__read-more-button {
|
||||
text-decoration: underline;
|
||||
|
|
|
@ -273,6 +273,7 @@ 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;
|
||||
|
@ -648,6 +649,7 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
.quote-indicator,
|
||||
.reply-indicator {
|
||||
background: transparent;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
|
@ -659,6 +661,7 @@ html {
|
|||
}
|
||||
|
||||
.status__content,
|
||||
.quote-indicator__content,
|
||||
.reply-indicator__content {
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
|
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 950 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 588 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.0 KiB |
|
@ -1,2 +1,138 @@
|
|||
<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>
|
||||
|
||||
<?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>
|
||||
|
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 12 KiB |
|
@ -38,6 +38,8 @@ const notFoundFn = () => (
|
|||
set='twitter'
|
||||
size={32}
|
||||
sheetSize={32}
|
||||
sheetColumns={60}
|
||||
sheetRows={60}
|
||||
backgroundImageFn={backgroundImageFn}
|
||||
/>
|
||||
|
||||
|
@ -96,12 +98,12 @@ class ModifierPickerMenu extends React.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} 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>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -136,7 +138,7 @@ class ModifierPicker extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown__modifiers'>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
|
||||
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||
</div>
|
||||
);
|
||||
|
@ -273,6 +275,8 @@ class EmojiPickerMenuImpl extends React.PureComponent {
|
|||
perLine={8}
|
||||
emojiSize={22}
|
||||
sheetSize={32}
|
||||
sheetColumns={60}
|
||||
sheetRows={60}
|
||||
custom={buildCustomEmojis(custom_emojis)}
|
||||
color=''
|
||||
emoji=''
|
||||
|
|
|
@ -108,24 +108,27 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
def process_status_params
|
||||
@status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url)
|
||||
|
||||
@params = {
|
||||
uri: @status_parser.uri,
|
||||
url: @status_parser.url || @status_parser.uri,
|
||||
account: @account,
|
||||
text: converted_object_type? ? converted_text : (@status_parser.text || ''),
|
||||
language: @status_parser.language,
|
||||
spoiler_text: converted_object_type? ? '' : (@status_parser.spoiler_text || ''),
|
||||
created_at: @status_parser.created_at,
|
||||
edited_at: @status_parser.edited_at && @status_parser.edited_at != @status_parser.created_at ? @status_parser.edited_at : nil,
|
||||
override_timestamps: @options[:override_timestamps],
|
||||
reply: @status_parser.reply,
|
||||
sensitive: @account.sensitized? || @status_parser.sensitive || false,
|
||||
visibility: @status_parser.visibility,
|
||||
thread: replied_to_status,
|
||||
conversation: conversation_from_uri(@object['conversation']),
|
||||
media_attachment_ids: process_attachments.take(4).map(&:id),
|
||||
poll: process_poll,
|
||||
}
|
||||
@params = begin
|
||||
{
|
||||
uri: @status_parser.uri,
|
||||
url: @status_parser.url || @status_parser.uri,
|
||||
account: @account,
|
||||
text: converted_object_type? ? converted_text : (@status_parser.text || ''),
|
||||
language: @status_parser.language,
|
||||
spoiler_text: converted_object_type? ? '' : (@status_parser.spoiler_text || ''),
|
||||
created_at: @status_parser.created_at,
|
||||
edited_at: @status_parser.edited_at && @status_parser.edited_at != @status_parser.created_at ? @status_parser.edited_at : nil,
|
||||
override_timestamps: @options[:override_timestamps],
|
||||
reply: @status_parser.reply,
|
||||
sensitive: @account.sensitized? || @status_parser.sensitive || false,
|
||||
visibility: @status_parser.visibility,
|
||||
thread: replied_to_status,
|
||||
conversation: conversation_from_uri(@object['conversation']),
|
||||
media_attachment_ids: process_attachments.take(4).map(&:id),
|
||||
poll: process_poll,
|
||||
quote: process_quote,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def process_audience
|
||||
|
@ -424,4 +427,28 @@ 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
|
||||
|
|
|
@ -14,6 +14,8 @@ module ActivityPub::CaseTransform
|
|||
when String
|
||||
camel_lower_cache[value] ||= if value.start_with?('_:')
|
||||
"_:#{value.gsub(/\A_:/, '').underscore.camelize(:lower)}"
|
||||
elsif value.start_with?('_')
|
||||
value
|
||||
else
|
||||
value.underscore.camelize(:lower)
|
||||
end
|
||||
|
|
|
@ -79,11 +79,15 @@ 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'
|
||||
[COLLECTIONS[:public]]
|
||||
to << COLLECTIONS[:public]
|
||||
when 'unlisted', 'private'
|
||||
[account_followers_url(status.account)]
|
||||
to << account_followers_url(status.account)
|
||||
when 'direct', 'limited'
|
||||
if status.account.silenced?
|
||||
# Only notify followers if the account is locally silenced
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
#
|
||||
# Table name: accounts
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# username :string default(""), not null
|
||||
# domain :string
|
||||
# private_key :text
|
||||
|
@ -17,11 +16,11 @@
|
|||
# url :string
|
||||
# avatar_file_name :string
|
||||
# avatar_content_type :string
|
||||
# avatar_file_size :integer
|
||||
# avatar_file_size :bigint(8)
|
||||
# avatar_updated_at :datetime
|
||||
# header_file_name :string
|
||||
# header_content_type :string
|
||||
# header_file_size :integer
|
||||
# header_file_size :bigint(8)
|
||||
# header_updated_at :datetime
|
||||
# avatar_remote_url :string
|
||||
# locked :boolean default(FALSE), not null
|
||||
|
@ -32,6 +31,7 @@
|
|||
# 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
|
||||
# suspension_origin :integer
|
||||
# sensitized_at :datetime
|
||||
# suspension_origin :integer
|
||||
# trendable :boolean
|
||||
# reviewed_at :datetime
|
||||
# requested_review_at :datetime
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
# domain :string
|
||||
# image_file_name :string
|
||||
# image_content_type :string
|
||||
# image_file_size :integer
|
||||
# image_file_size :bigint(8)
|
||||
# image_updated_at :datetime
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
|
|
|
@ -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 :integer
|
||||
# data_file_size :bigint(8)
|
||||
# data_updated_at :datetime
|
||||
# account_id :bigint(8) not null
|
||||
# id :bigint(8) not null, primary key
|
||||
# overwrite :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
#
|
||||
# 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 :integer
|
||||
# file_file_size :bigint(8)
|
||||
# file_updated_at :datetime
|
||||
# remote_url :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
|
@ -17,6 +16,7 @@
|
|||
# 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 :integer
|
||||
# thumbnail_file_size :bigint(8)
|
||||
# thumbnail_updated_at :datetime
|
||||
# thumbnail_remote_url :string
|
||||
#
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
# description :string default(""), not null
|
||||
# image_file_name :string
|
||||
# image_content_type :string
|
||||
# image_file_size :integer
|
||||
# image_file_size :bigint(8)
|
||||
# image_updated_at :datetime
|
||||
# type :integer default("link"), not null
|
||||
# html :text default(""), not null
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
# var :string default(""), not null
|
||||
# file_file_name :string
|
||||
# file_content_type :string
|
||||
# file_file_size :integer
|
||||
# file_file_size :bigint(8)
|
||||
# file_updated_at :datetime
|
||||
# meta :json
|
||||
# created_at :datetime not null
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
# edited_at :datetime
|
||||
# trendable :boolean
|
||||
# ordered_media_attachment_ids :bigint(8) is an Array
|
||||
# quote_id :bigint(8)
|
||||
#
|
||||
|
||||
class Status < ApplicationRecord
|
||||
|
@ -62,6 +63,7 @@ class Status < ApplicationRecord
|
|||
|
||||
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
|
||||
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
|
||||
belongs_to :quote, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quote, optional: true
|
||||
|
||||
has_many :favourites, inverse_of: :status, dependent: :destroy
|
||||
has_many :bookmarks, inverse_of: :status, dependent: :destroy
|
||||
|
@ -72,6 +74,7 @@ class Status < ApplicationRecord
|
|||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
|
||||
has_many :media_attachments, dependent: :nullify
|
||||
has_many :quoted, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quote, dependent: :nullify
|
||||
|
||||
has_and_belongs_to_many :tags
|
||||
has_and_belongs_to_many :preview_cards
|
||||
|
@ -88,6 +91,7 @@ class Status < ApplicationRecord
|
|||
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
|
||||
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
|
||||
validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true
|
||||
validates :quote_visibility, inclusion: { in: %w(public unlisted) }, if: :quote?
|
||||
|
||||
accepts_nested_attributes_for :poll
|
||||
|
||||
|
@ -139,6 +143,17 @@ class Status < ApplicationRecord
|
|||
account: [:account_stat, user: :role],
|
||||
active_mentions: { account: :account_stat },
|
||||
],
|
||||
quote: [
|
||||
:application,
|
||||
:tags,
|
||||
:preview_cards,
|
||||
:media_attachments,
|
||||
:conversation,
|
||||
:status_stat,
|
||||
:preloadable_poll,
|
||||
account: [:account_stat, :user],
|
||||
active_mentions: { account: :account_stat },
|
||||
],
|
||||
thread: { account: :account_stat }
|
||||
|
||||
delegate :domain, to: :account, prefix: true
|
||||
|
@ -204,6 +219,14 @@ class Status < ApplicationRecord
|
|||
!reblog_of_id.nil?
|
||||
end
|
||||
|
||||
def quote?
|
||||
!quote_id.nil? && quote
|
||||
end
|
||||
|
||||
def quote_visibility
|
||||
quote&.visibility
|
||||
end
|
||||
|
||||
def within_realtime_window?
|
||||
created_at >= REAL_TIME_WINDOW.ago
|
||||
end
|
||||
|
@ -268,7 +291,7 @@ class Status < ApplicationRecord
|
|||
fields = [spoiler_text, text]
|
||||
fields += preloadable_poll.options unless preloadable_poll.nil?
|
||||
|
||||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
|
||||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain) + (quote? ? CustomEmoji.from_text([quote.spoiler_text, quote.text].join(' '), quote.account.domain) : [])
|
||||
end
|
||||
|
||||
def ordered_media_attachments
|
||||
|
|
|
@ -62,6 +62,10 @@ class StatusEdit < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def quote?
|
||||
status.quote?
|
||||
end
|
||||
|
||||
def proper
|
||||
self
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
include FormattingHelper
|
||||
|
||||
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message
|
||||
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message, :quote_uri
|
||||
|
||||
attributes :id, :type, :summary,
|
||||
:in_reply_to, :published, :url,
|
||||
|
@ -11,6 +11,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||
:atom_uri, :in_reply_to_atom_uri,
|
||||
:conversation
|
||||
|
||||
attribute :quote_uri, if: -> { object.quote? }
|
||||
|
||||
attribute :content
|
||||
attribute :content_map, if: :language?
|
||||
attribute :updated, if: :edited?
|
||||
|
@ -150,6 +152,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||
end
|
||||
end
|
||||
|
||||
def quote_uri
|
||||
ActivityPub::TagManager.instance.uri_for(object.quote) if object.quote?
|
||||
end
|
||||
|
||||
def local?
|
||||
object.account.local?
|
||||
end
|
||||
|
|
|
@ -186,3 +186,13 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class REST::QuoteStatusSerializer < REST::StatusSerializer
|
||||
attribute :quote do
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
class REST::StatusSerializer < ActiveModel::Serializer
|
||||
belongs_to :quote, serializer: REST::QuoteStatusSerializer
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'digest'
|
||||
|
||||
class ActivityPub::ProcessAccountService < BaseService
|
||||
include JsonLdHelper
|
||||
include DomainControlHelper
|
||||
|
@ -87,6 +89,9 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
set_immediate_protocol_attributes!
|
||||
set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local?
|
||||
set_immediate_attributes! unless @account.suspended?
|
||||
|
||||
TreehouseAutomodExt.heuristic_auto_suspend!(@account)
|
||||
|
||||
set_fetchable_attributes! unless @options[:only_key] || @account.suspended?
|
||||
|
||||
@account.save_with_optional_media!
|
||||
|
@ -334,4 +339,88 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
emoji.image_remote_url = image_url
|
||||
emoji.save
|
||||
end
|
||||
|
||||
module TreehouseAutomodExt
|
||||
HEURISTIC_AUTO_SUSPEND_ACTIVE = ENV.fetch('TH_HEURISTIC_AUTO_SUSPEND', '') == 'that-one-spammer'
|
||||
AUTOMOD_ACCOUNT_USERNAME = ENV['TH_STAFF_ACCOUNT']
|
||||
|
||||
# hardcoded for now
|
||||
# md5 because they don't deserve more mentions
|
||||
HEURISTIC_NAMES = {
|
||||
'0116a9deace3289b7092e945ef5ca0a5' => Set['57d3d0b932cc9cd01be6b2f4e82c1a4a']
|
||||
}
|
||||
# probably mathematically impossible to collide, but just in case...
|
||||
HEURISTIC_MAX_LEN = 16
|
||||
|
||||
COMMENT_HEADER = <<~EOS
|
||||
Tracking Report - automatically created by TreehouseAutomodExt
|
||||
EOS
|
||||
|
||||
WARNING_TEXT = <<~EOS
|
||||
Tracking Infraction - automatically created by TreehouseAutomodExt
|
||||
EOS
|
||||
|
||||
EXPLANATION = <<~EOS
|
||||
This account was automatically suspended by TreehouseAutomodExt, an unsupported feature of Treehouse Social.
|
||||
|
||||
Currently, the heuristic should only automatically suspend accounts with one specific username and display name.
|
||||
|
||||
If this action is unexpected, please unset TH_HEURISTIC_AUTO_SUSPEND.
|
||||
EOS
|
||||
|
||||
def self.heuristic_auto_suspend?(account)
|
||||
return false unless HEURISTIC_AUTO_SUSPEND_ACTIVE
|
||||
|
||||
return unless account.username.length < HEURISTIC_MAX_LEN && account.display_name.length < HEURISTIC_MAX_LEN
|
||||
|
||||
username_md5 = Digest::MD5.hexdigest(account.username)
|
||||
display_name_md5 = Digest::MD5.hexdigest(account.display_name)
|
||||
|
||||
HEURISTIC_NAMES[username_md5].include?(display_name_md5)
|
||||
end
|
||||
|
||||
def self.heuristic_auto_suspend!(account)
|
||||
return unless heuristic_auto_suspend?(account)
|
||||
|
||||
file_tracking_report!(account) unless account.suspension_origin == :local
|
||||
|
||||
account.suspended_at = Time.now.utc unless account.suspension_origin == :local
|
||||
account.suspension_origin = :local
|
||||
account.save!
|
||||
end
|
||||
|
||||
def self.file_tracking_report!(account)
|
||||
reporter = staff_account
|
||||
return unless reporter
|
||||
|
||||
report = ReportService.new.call(
|
||||
reporter,
|
||||
account,
|
||||
{
|
||||
comment: "#{COMMENT_HEADER}\n\n#{EXPLANATION}",
|
||||
th_skip_notify_staff: true,
|
||||
th_skip_forward: true,
|
||||
}
|
||||
)
|
||||
report.spam!
|
||||
report.assign_to_self!(reporter)
|
||||
|
||||
|
||||
account_action = Admin::AccountAction.new(
|
||||
type: 'suspend',
|
||||
report_id: report.id,
|
||||
target_account: account,
|
||||
current_account: reporter,
|
||||
send_email_notification: false,
|
||||
text: WARNING_TEXT,
|
||||
)
|
||||
account_action.save!
|
||||
|
||||
report.resolve!(reporter)
|
||||
end
|
||||
|
||||
def self.staff_account
|
||||
Account.find_local(AUTOMOD_ACCOUNT_USERNAME) if AUTOMOD_ACCOUNT_USERNAME
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -73,7 +73,7 @@ class FetchLinkCardService < BaseService
|
|||
@status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
|
||||
else
|
||||
document = Nokogiri::HTML(@status.text)
|
||||
links = document.css('a')
|
||||
links = document.css(':not(.quote-inline) > a')
|
||||
|
||||
links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
|
||||
end
|
||||
|
|
|
@ -31,6 +31,7 @@ class PostStatusService < BaseService
|
|||
# @option [String] :idempotency Optional idempotency key
|
||||
# @option [Boolean] :with_rate_limit
|
||||
# @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions
|
||||
# @option [String] :quote_id
|
||||
# @return [Status]
|
||||
def call(account, options = {})
|
||||
@account = account
|
||||
|
@ -211,6 +212,7 @@ class PostStatusService < BaseService
|
|||
application: @options[:application],
|
||||
content_type: @options[:content_type] || @account.user&.setting_default_content_type,
|
||||
rate_limit: @options[:with_rate_limit],
|
||||
quote_id: @options[:quote_id],
|
||||
}.compact
|
||||
end
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ class ReportService < BaseService
|
|||
end
|
||||
|
||||
def notify_staff!
|
||||
return if @options[:th_skip_notify_staff]
|
||||
return if @report.unresolved_siblings?
|
||||
|
||||
User.those_who_can(:manage_reports).includes(:account).each do |u|
|
||||
|
@ -53,6 +54,7 @@ class ReportService < BaseService
|
|||
end
|
||||
|
||||
def forward?
|
||||
return false if @options[:th_skip_forward]
|
||||
!@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||
end
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
- %w(57 60 72 76 114 120 144 152 167 180 1024).each do |size|
|
||||
%link{ rel: 'apple-touch-icon', sizes: "#{size}x#{size}", href: asset_pack_path("media/icons/apple-touch-icon-#{size}x#{size}.png") }/
|
||||
|
||||
%link{ rel: 'mask-icon', href: asset_pack_path('media/images/logo-symbol-icon.svg'), color: '#6364FF' }/
|
||||
%link{ rel: 'mask-icon', href: asset_pack_path('media/images/logo-symbol-icon.svg'), color: '#00AEEF' }/
|
||||
%link{ rel: 'manifest', href: manifest_path(format: :json) }/
|
||||
%meta{ name: 'theme-color', content: '#191b22' }/
|
||||
%meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
|
||||
|
|
|
@ -15,7 +15,11 @@
|
|||
|
||||
= account_action_button(status.account)
|
||||
|
||||
- if status.quote?
|
||||
= render partial: "statuses/quote_status", locals: {status: status.quote}
|
||||
|
||||
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
||||
|
||||
- if status.spoiler_text?
|
||||
%p<
|
||||
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
.status.quote-status{ dataurl: ActivityPub::TagManager.instance.url_for(status) }
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do
|
||||
.status__avatar
|
||||
%div
|
||||
= image_tag status.account.avatar_static_url, width: 18, height: 18, alt: '', class: 'u-photo account__avatar'
|
||||
%span.display-name
|
||||
%bdi
|
||||
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true)
|
||||
|
||||
%span.display-name__account
|
||||
= acct(status.account)
|
||||
= fa_icon('lock') if status.account.locked?
|
||||
|
||||
.status__content.emojify<
|
||||
- if status.spoiler_text?
|
||||
%p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
|
||||
%span.p-summary> #{Formatter.instance.format_spoiler(status)}
|
||||
%button.status__content__spoiler-link= t('statuses.show_more')
|
||||
.e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}" }
|
||||
= Formatter.instance.format_in_quote(status, custom_emojify: true)
|
||||
|
||||
- if !status.media_attachments.empty?
|
||||
- if status.media_attachments.first.video?
|
||||
- video = status.media_attachments.first
|
||||
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description, quote: true do
|
||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
- elsif status.media_attachments.first.audio?
|
||||
- audio = status.media_attachments.first
|
||||
= react_component :audio, src: audio.file.url(:original), height: 60, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do
|
||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
- else
|
||||
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }, quote: true do
|
||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
- elsif status.preview_card
|
||||
= react_component :card, maxDescription: 10, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json, quote: true
|
|
@ -27,7 +27,12 @@
|
|||
%span.display-name__account
|
||||
= acct(status.account)
|
||||
= fa_icon('lock') if status.account.locked?
|
||||
|
||||
- if status.quote?
|
||||
= render partial: "statuses/quote_status", locals: {status: status.quote}
|
||||
|
||||
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
||||
|
||||
- if status.spoiler_text?
|
||||
%p<
|
||||
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
||||
|
|
|
@ -103,7 +103,7 @@ en:
|
|||
imports:
|
||||
data: CSV file exported from another Mastodon server
|
||||
invite_request:
|
||||
text: This will help us review your application
|
||||
text: "Be sure to provide at least one link to an established account on another social website: GitHub, Twitter, or a personal blog or website. This will help us review your request in a timely manner."
|
||||
ip_block:
|
||||
comment: Optional. Remember why you added this rule.
|
||||
expires_in: IP addresses are a finite resource, they are sometimes shared and often change hands. For this reason, indefinite IP blocks are not recommended.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddQuoteIdToStatuses < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :statuses, :quote_id, :bigint, null: true, default: nil
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
class AddIndexToStatusesQuoteId < ActiveRecord::Migration[6.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_index :statuses, :quote_id, algorithm: :concurrently
|
||||
end
|
||||
end
|
30
db/schema.rb
|
@ -153,11 +153,11 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.string "url"
|
||||
t.string "avatar_file_name"
|
||||
t.string "avatar_content_type"
|
||||
t.integer "avatar_file_size"
|
||||
t.bigint "avatar_file_size"
|
||||
t.datetime "avatar_updated_at"
|
||||
t.string "header_file_name"
|
||||
t.string "header_content_type"
|
||||
t.integer "header_file_size"
|
||||
t.bigint "header_file_size"
|
||||
t.datetime "header_updated_at"
|
||||
t.string "avatar_remote_url"
|
||||
t.boolean "locked", default: false, null: false
|
||||
|
@ -181,8 +181,8 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.integer "avatar_storage_schema_version"
|
||||
t.integer "header_storage_schema_version"
|
||||
t.string "devices_url"
|
||||
t.integer "suspension_origin"
|
||||
t.datetime "sensitized_at"
|
||||
t.integer "suspension_origin"
|
||||
t.boolean "trendable"
|
||||
t.datetime "reviewed_at"
|
||||
t.datetime "requested_review_at"
|
||||
|
@ -328,7 +328,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.string "domain"
|
||||
t.string "image_file_name"
|
||||
t.string "image_content_type"
|
||||
t.integer "image_file_size"
|
||||
t.bigint "image_file_size"
|
||||
t.datetime "image_updated_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
|
@ -495,7 +495,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.datetime "updated_at", null: false
|
||||
t.string "data_file_name"
|
||||
t.string "data_content_type"
|
||||
t.integer "data_file_size"
|
||||
t.bigint "data_file_size"
|
||||
t.datetime "data_updated_at"
|
||||
t.bigint "account_id", null: false
|
||||
t.boolean "overwrite", default: false, null: false
|
||||
|
@ -516,12 +516,12 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
end
|
||||
|
||||
create_table "ip_blocks", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "expires_at"
|
||||
t.inet "ip", default: "0.0.0.0", null: false
|
||||
t.integer "severity", default: 0, null: false
|
||||
t.datetime "expires_at"
|
||||
t.text "comment", default: "", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true
|
||||
end
|
||||
|
||||
|
@ -569,7 +569,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.bigint "status_id"
|
||||
t.string "file_file_name"
|
||||
t.string "file_content_type"
|
||||
t.integer "file_file_size"
|
||||
t.bigint "file_file_size"
|
||||
t.datetime "file_updated_at"
|
||||
t.string "remote_url", default: "", null: false
|
||||
t.datetime "created_at", null: false
|
||||
|
@ -585,7 +585,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.integer "file_storage_schema_version"
|
||||
t.string "thumbnail_file_name"
|
||||
t.string "thumbnail_content_type"
|
||||
t.integer "thumbnail_file_size"
|
||||
t.bigint "thumbnail_file_size"
|
||||
t.datetime "thumbnail_updated_at"
|
||||
t.string "thumbnail_remote_url"
|
||||
t.index ["account_id", "status_id"], name: "index_media_attachments_on_account_id_and_status_id", order: { status_id: :desc }
|
||||
|
@ -752,7 +752,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.string "description", default: "", null: false
|
||||
t.string "image_file_name"
|
||||
t.string "image_content_type"
|
||||
t.integer "image_file_size"
|
||||
t.bigint "image_file_size"
|
||||
t.datetime "image_updated_at"
|
||||
t.integer "type", default: 0, null: false
|
||||
t.text "html", default: "", null: false
|
||||
|
@ -863,7 +863,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.string "var", default: "", null: false
|
||||
t.string "file_file_name"
|
||||
t.string "file_content_type"
|
||||
t.integer "file_file_size"
|
||||
t.bigint "file_file_size"
|
||||
t.datetime "file_updated_at"
|
||||
t.json "meta"
|
||||
t.datetime "created_at", null: false
|
||||
|
@ -891,8 +891,8 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
create_table "status_pins", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.bigint "status_id", null: false
|
||||
t.datetime "created_at", default: -> { "now()" }, null: false
|
||||
t.datetime "updated_at", default: -> { "now()" }, null: false
|
||||
t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||
t.datetime "updated_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||
t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true
|
||||
t.index ["status_id"], name: "index_status_pins_on_status_id"
|
||||
end
|
||||
|
@ -943,6 +943,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.datetime "edited_at"
|
||||
t.boolean "trendable"
|
||||
t.bigint "ordered_media_attachment_ids", array: true
|
||||
t.bigint "quote_id"
|
||||
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
|
||||
t.index ["account_id"], name: "index_statuses_on_account_id"
|
||||
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
|
||||
|
@ -950,6 +951,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
|
|||
t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
||||
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
|
||||
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
|
||||
t.index ["quote_id"], name: "index_statuses_on_quote_id"
|
||||
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
|
||||
t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
|
||||
end
|
||||
|
|
|
@ -2,16 +2,17 @@ version: '3'
|
|||
services:
|
||||
db:
|
||||
restart: always
|
||||
image: postgres:14-alpine
|
||||
image: postgres:15-alpine
|
||||
shm_size: 256mb
|
||||
networks:
|
||||
- internal_network
|
||||
healthcheck:
|
||||
test: ['CMD', 'pg_isready', '-U', 'postgres']
|
||||
volumes:
|
||||
- ./postgres14:/var/lib/postgresql/data
|
||||
- ./data/postgres.15:/var/lib/postgresql/data
|
||||
environment:
|
||||
- 'POSTGRES_HOST_AUTH_METHOD=trust'
|
||||
- 'POSTGRES_USER=mastodon'
|
||||
|
||||
redis:
|
||||
restart: always
|
||||
|
@ -21,7 +22,7 @@ services:
|
|||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
volumes:
|
||||
- ./redis:/data
|
||||
- ./data/redis:/data
|
||||
|
||||
# es:
|
||||
# restart: always
|
||||
|
@ -56,7 +57,7 @@ services:
|
|||
|
||||
web:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon
|
||||
image: gitea.treehouse.systems/treehouse/mastodon:latest
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||
|
@ -66,18 +67,29 @@ services:
|
|||
healthcheck:
|
||||
# prettier-ignore
|
||||
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1']
|
||||
expose:
|
||||
- 3000
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000'
|
||||
- 3000:3000
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.web.rule=Host(`social-dev.treehouse.systems`)
|
||||
- traefik.http.routers.web.tls=true
|
||||
- traefik.http.routers.web.tls.certresolver=le
|
||||
- traefik.http.routers.web.tls.domains[0].main=social-dev.treehouse.systems
|
||||
- traefik.http.routers.web.entrypoints=websecure
|
||||
- traefik.http.services.web.loadbalancer.server.port=3000
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
# - es
|
||||
volumes:
|
||||
- ./public/system:/mastodon/public/system
|
||||
# - ./data/postgres:/var/lib/postgresql/data
|
||||
|
||||
streaming:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon
|
||||
image: gitea.treehouse.systems/treehouse/mastodon:latest
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: node ./streaming
|
||||
|
@ -87,15 +99,23 @@ services:
|
|||
healthcheck:
|
||||
# prettier-ignore
|
||||
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1']
|
||||
ports:
|
||||
- '127.0.0.1:4000:4000'
|
||||
expose:
|
||||
- 4000
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- 'traefik.http.routers.streaming.rule=Host(`social-dev.treehouse.systems`) && PathPrefix(`/api/v1/streaming/`)'
|
||||
- traefik.http.routers.streaming.tls=true
|
||||
- traefik.http.routers.streaming.tls.certresolver=le
|
||||
- traefik.http.routers.streaming.tls.domains[0].main=social-dev.treehouse.systems
|
||||
- traefik.http.routers.streaming.entrypoints=websecure
|
||||
- traefik.http.services.streaming.loadbalancer.server.port=4000
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
sidekiq:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon
|
||||
image: gitea.treehouse.systems/treehouse/mastodon:latest
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
|
|
@ -1,10 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'pathname'
|
||||
|
||||
def setup_redis_env_url(prefix = nil, defaults = true)
|
||||
prefix = "#{prefix.to_s.upcase}_" unless prefix.nil?
|
||||
prefix = '' if prefix.nil?
|
||||
redis_url_key = "#{prefix}REDIS_URL"
|
||||
|
||||
return if ENV["#{prefix}REDIS_URL"].present?
|
||||
if ENV[redis_url_key].present?
|
||||
conn = +ENV["#{prefix}REDIS_URL"].sub(/redis:\/\//i, '')
|
||||
|
||||
# Strip any prefixing `unix://`
|
||||
unix = !conn.sub!(/^unix:\/\//i, '').nil?
|
||||
# Strip any prefixing `./`
|
||||
unix |= conn.sub!(/^(\.\/)+/, '')
|
||||
unix |= conn.start_with?('/')
|
||||
|
||||
if unix
|
||||
pn = Pathname.new(conn)
|
||||
pn = Pathname.getwd / pn if pn.relative?
|
||||
ENV[redis_url_key] = "unix://#{pn}"
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
password = ENV.fetch("#{prefix}REDIS_PASSWORD") { '' if defaults }
|
||||
host = ENV.fetch("#{prefix}REDIS_HOST") { 'localhost' if defaults }
|
||||
|
|
|
@ -21,7 +21,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def suffix
|
||||
'+glitch'
|
||||
'+glitch.th'
|
||||
end
|
||||
|
||||
def to_a
|
||||
|
@ -33,21 +33,30 @@ module Mastodon
|
|||
end
|
||||
|
||||
def repository
|
||||
ENV.fetch('GITHUB_REPOSITORY', 'glitch-soc/mastodon')
|
||||
ENV.fetch('GIT_REPOSITORY', false) || ENV.fetch('GITHUB_REPOSITORY', false) || 'treehouse/mastodon'
|
||||
end
|
||||
|
||||
def source_base_url
|
||||
ENV.fetch('SOURCE_BASE_URL', "https://github.com/#{repository}")
|
||||
base = ENV['GITHUB_REPOSITORY'] ? 'https://github.com' : 'https://gitea.treehouse.systems'
|
||||
ENV.fetch('SOURCE_BASE_URL', "#{base}/#{repository}")
|
||||
end
|
||||
|
||||
# specify git tag or commit hash here
|
||||
def source_tag
|
||||
ENV.fetch('SOURCE_TAG', nil)
|
||||
tag = ENV.fetch('SOURCE_TAG', nil)
|
||||
return if tag.nil? || tag.empty?
|
||||
tag
|
||||
end
|
||||
|
||||
def source_url
|
||||
if source_tag
|
||||
"#{source_base_url}/tree/#{source_tag}"
|
||||
tag = source_tag
|
||||
if tag && source_base_url =~ /gitea/
|
||||
suffix = if !tag[/\H/]
|
||||
"commit/#{tag}"
|
||||
else
|
||||
"branch/#{tag}"
|
||||
end
|
||||
"#{source_base_url}/#{suffix}"
|
||||
else
|
||||
source_base_url
|
||||
end
|
||||
|
|
|
@ -31,6 +31,7 @@ class Sanitize
|
|||
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
|
||||
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
|
||||
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
|
||||
next true if /^quote-inline$/.match?(e) # quote inline classes
|
||||
end
|
||||
|
||||
node['class'] = class_list.join(' ')
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'pathname'
|
||||
|
||||
DATA_DIR = Pathname.new('data')
|
||||
POSTGRES_DIR = DATA_DIR / 'postgres'
|
||||
POSTGRES_CONF_FILE = POSTGRES_DIR / 'postgresql.conf'
|
||||
POSTGRES_SOCKET_FILE = POSTGRES_DIR / '.s.PGSQL.5432'
|
||||
POSTGRES_PID_FILE = POSTGRES_DIR / 'postmaster.pid'
|
||||
REDIS_DIR = DATA_DIR / 'redis'
|
||||
REDIS_PID_FILE = REDIS_DIR / 'redis-dev.pid'
|
||||
|
||||
def divider
|
||||
puts '=========='
|
||||
end
|
||||
|
||||
def get_pid(pid_file)
|
||||
return false unless File.file?(pid_file)
|
||||
pid = File.read(pid_file).to_i
|
||||
|
||||
Process.kill(0, pid)
|
||||
pid
|
||||
rescue Errno::ESRCH
|
||||
nil
|
||||
end
|
||||
|
||||
def postgres_running?
|
||||
get_pid POSTGRES_PID_FILE
|
||||
end
|
||||
|
||||
directory REDIS_DIR.to_s
|
||||
|
||||
namespace :deps do
|
||||
task start: ['postgres:start', 'redis:start']
|
||||
task stop: ['postgres:stop', 'redis:stop']
|
||||
|
||||
namespace :postgres do
|
||||
namespace :setup do
|
||||
task all: [POSTGRES_DIR.to_s]
|
||||
|
||||
file POSTGRES_DIR.to_s do
|
||||
if POSTGRES_CONF_FILE.exist?
|
||||
puts 'Postgres conf exists, skipping initdb'
|
||||
next
|
||||
end
|
||||
sh %(printf '%s\\n' pg_ctl -D data/postgres initdb -o '-U mastodon --auth-host=trust')
|
||||
end
|
||||
|
||||
task configure: [POSTGRES_DIR.to_s] do
|
||||
next if File.foreach(POSTGRES_CONF_FILE).detect? { |line| line == /^unix_socket_directories = \.\s*$/ }
|
||||
|
||||
POSTGRES_CONF_FILE.open('at') do |f|
|
||||
f.write("\n", PG_SOCKET_DIRECTORIES_LINE, "\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
task start: ['setup:all'] do
|
||||
if (pid = get_pid POSTGRES_PID_FILE)
|
||||
puts "Postgres is running (pid #{pid})!"
|
||||
next
|
||||
end
|
||||
|
||||
puts 'Starting postgres...'
|
||||
divider
|
||||
sh %(pg_ctl -D ./data/postgres start)
|
||||
divider
|
||||
end
|
||||
|
||||
task :stop do
|
||||
unless (pid = get_pid POSTGRES_PID_FILE)
|
||||
puts "Postgres isn't running!"
|
||||
next
|
||||
end
|
||||
|
||||
puts "Stopping Postgres (pid #{pid})..."
|
||||
sh %(pg_ctl -D ./data/postgres stop)
|
||||
divider
|
||||
end
|
||||
end
|
||||
|
||||
namespace :redis do
|
||||
task init: [REDIS_DIR.to_s] do
|
||||
end
|
||||
|
||||
task start: [:init] do
|
||||
if (pid = get_pid REDIS_PID_FILE)
|
||||
puts "Redis is running (pid #{pid})!"
|
||||
next
|
||||
end
|
||||
|
||||
puts 'Starting redis...'
|
||||
divider
|
||||
sh %(redis-server redis-dev.conf)
|
||||
divider
|
||||
end
|
||||
|
||||
task :stop do
|
||||
unless (pid = get_pid REDIS_PID_FILE)
|
||||
puts "Redis isn't running!"
|
||||
next
|
||||
end
|
||||
|
||||
puts "Stopping Redis (pid #{pid})..."
|
||||
divider
|
||||
Process.kill(:TERM, pid)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -38,8 +38,8 @@
|
|||
"@github/webauthn-json": "^2.1.1",
|
||||
"@rails/ujs": "^6.1.7",
|
||||
"abortcontroller-polyfill": "^1.7.5",
|
||||
"atrament": "0.2.4",
|
||||
"arrow-key-navigation": "^1.2.0",
|
||||
"atrament": "0.2.4",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"axios": "^1.3.5",
|
||||
"babel-loader": "^8.3.0",
|
||||
|
|
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 2.6 KiB |