forked from treehouse/mastodon
Compare commits
42 Commits
treehouse-
...
main
Author | SHA1 | Date |
---|---|---|
Ariadne Conill | 384ac613d8 | |
Ariadne Conill | c6a4f42a37 | |
Ariadne Conill | 8b6e2ed562 | |
Ariadne Conill | c4253c32a0 | |
Ariadne Conill | df07456f51 | |
Ariadne Conill | 7efe4bc5d3 | |
Ariadne Conill | f0065720d6 | |
Ariadne Conill | d23cd8da00 | |
Ariadne Conill | 67a7b6067a | |
Ariadne Conill | 7d4127065d | |
Ariadne Conill | e59c40eb68 | |
Ariadne Conill | 5a8d4265ef | |
Ariadne Conill | 214a4c9e6b | |
Ariadne Conill | 766a643811 | |
Ariadne Conill | a47d917072 | |
Ariadne Conill | c7e00d4c4e | |
Ariadne Conill | 9d4851e3cd | |
Ariadne Conill | adf1e9fc2e | |
Ariadne Conill | 08aecd24ba | |
Ariadne Conill | 005256ae8c | |
Ariadne Conill | 5be6a59f80 | |
Ariadne Conill | 0d3df3e8cf | |
Ariadne Conill | b36e884cc1 | |
Ariadne Conill | 14d001574c | |
Ariadne Conill | 61565488a6 | |
Ariadne Conill | 8d86c77a58 | |
Ariadne Conill | 36955a7a56 | |
Ariadne Conill | 1cef1eb847 | |
Ariadne Conill | a697e1da13 | |
Ariadne Conill | 0b48ae2c3c | |
Ariadne Conill | 1df2577b89 | |
Ariadne Conill | 28fb5c8c52 | |
Ariadne Conill | 56d4b04358 | |
Ariadne Conill | ee98c0a6f8 | |
Ariadne Conill | ba965bec3d | |
Ariadne Conill | 968bd6f0ee | |
Ariadne Conill | 6b07407820 | |
Ariadne Conill | 0990d5ac75 | |
Ariadne Conill | b1bce9d193 | |
Ariadne Conill | 78e8693388 | |
Ariadne Conill | 6be7cbb0bc | |
Ariadne Conill | 4bb0e9f9ed |
|
@ -1,5 +1,6 @@
|
||||||
[production]
|
[production]
|
||||||
defaults
|
defaults
|
||||||
|
not IE 11
|
||||||
not dead
|
not dead
|
||||||
|
|
||||||
[development]
|
[development]
|
||||||
|
|
|
@ -19,4 +19,3 @@ postgres14
|
||||||
redis
|
redis
|
||||||
elasticsearch
|
elasticsearch
|
||||||
chart
|
chart
|
||||||
data
|
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -3,9 +3,6 @@
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '>= 2.6.0', '< 3.1.0'
|
ruby '>= 2.6.0', '< 3.1.0'
|
||||||
|
|
||||||
|
|
||||||
gem 'psych', '< 4'
|
|
||||||
|
|
||||||
gem 'pkg-config', '~> 1.4'
|
gem 'pkg-config', '~> 1.4'
|
||||||
gem 'rexml', '~> 3.2'
|
gem 'rexml', '~> 3.2'
|
||||||
|
|
||||||
|
|
|
@ -478,7 +478,6 @@ GEM
|
||||||
pry (>= 0.13, < 0.15)
|
pry (>= 0.13, < 0.15)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
psych (3.3.4)
|
|
||||||
public_suffix (5.0.0)
|
public_suffix (5.0.0)
|
||||||
puma (5.6.5)
|
puma (5.6.5)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
|
@ -814,7 +813,6 @@ DEPENDENCIES
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
pry-byebug (~> 3.10)
|
pry-byebug (~> 3.10)
|
||||||
pry-rails (~> 0.3)
|
pry-rails (~> 0.3)
|
||||||
psych (< 4)
|
|
||||||
puma (~> 5.6)
|
puma (~> 5.6)
|
||||||
pundit (~> 2.2)
|
pundit (~> 2.2)
|
||||||
rack (~> 2.2.4)
|
rack (~> 2.2.4)
|
||||||
|
@ -860,9 +858,3 @@ DEPENDENCIES
|
||||||
webpacker (~> 5.4)
|
webpacker (~> 5.4)
|
||||||
webpush!
|
webpush!
|
||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
||||||
RUBY VERSION
|
|
||||||
ruby 3.0.4p208
|
|
||||||
|
|
||||||
BUNDLED WITH
|
|
||||||
2.3.26
|
|
||||||
|
|
|
@ -1,100 +1,137 @@
|
||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
|
|
||||||
REQUIRED_PLUGINS = %w(vagrant-libvirt)
|
ENV["PORT"] ||= "3000"
|
||||||
exit unless REQUIRED_PLUGINS.all? do |plugin|
|
|
||||||
Vagrant.has_plugin?(plugin) || (
|
|
||||||
puts "The #{plugin} plugin is required. Please install it with:"
|
|
||||||
puts "$ vagrant plugin install #{plugin}"
|
|
||||||
false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
$provision = <<SCRIPT
|
$provision = <<SCRIPT
|
||||||
# see SETUP.md for details
|
|
||||||
|
|
||||||
export RAILS_ENV=development
|
cd /vagrant # This is where the host folder/repo is mounted
|
||||||
export NODE_ENV=development
|
|
||||||
export NODE_OPTIONS=--openssl-legacy-provider
|
|
||||||
|
|
||||||
echo '[treehouse-vagrant] install packages'
|
# Add the yarn repo + yarn repo keys
|
||||||
sudo pacman -Syu --noconfirm base-devel git libidn
|
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||||
sudo pacman -Syu --noconfirm ruby ruby-bundler ruby-irb redis postgresql yarn
|
sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
|
||||||
git clone https://aur.archlinux.org/ruby-foreman.git
|
|
||||||
cd ruby-foreman
|
|
||||||
yes | sudo -u vagrant makepkg -si
|
|
||||||
|
|
||||||
# treehouse mastodon files are synced here
|
# Add repo for NodeJS
|
||||||
cd /vagrant
|
curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -
|
||||||
|
|
||||||
echo '[treehouse-vagrant] init database'
|
# Add firewall rule to redirect 80 to PORT and save
|
||||||
if [ -d "data/" ]; then rm -rf data/; fi
|
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
|
||||||
mkdir -p data/
|
echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
|
||||||
pg_ctl -D data/postgres initdb -o '-U mastodon --auth-host=trust'
|
echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections
|
||||||
echo 'unix_socket_directories = .' >> data/postgres/postgresql.conf
|
sudo apt-get install iptables-persistent -y
|
||||||
pg_ctl -D data/postgres start --silent
|
|
||||||
|
|
||||||
echo '[treehouse-vagrant] start redis'
|
# Add packages to build and run Mastodon
|
||||||
mkdir -p data/redis
|
sudo apt-get install \
|
||||||
redis-server ./redis-dev.conf
|
git-core \
|
||||||
|
g++ \
|
||||||
|
libpq-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
imagemagick \
|
||||||
|
nodejs \
|
||||||
|
redis-server \
|
||||||
|
redis-tools \
|
||||||
|
postgresql \
|
||||||
|
postgresql-contrib \
|
||||||
|
yarn \
|
||||||
|
libicu-dev \
|
||||||
|
libidn11-dev \
|
||||||
|
libreadline-dev \
|
||||||
|
libpam0g-dev \
|
||||||
|
-y
|
||||||
|
|
||||||
echo '[treehouse-vagrant] bundle install'
|
# Install rvm
|
||||||
if [ -d "vendor/bundle/" ]; then rm -rf vendor/bundle/; fi
|
read RUBY_VERSION < .ruby-version
|
||||||
bundle config set --local path 'vendor/bundle/'
|
|
||||||
|
curl -sSL https://rvm.io/mpapis.asc | gpg --import
|
||||||
|
curl -sSL https://rvm.io/pkuczynski.asc | gpg --import
|
||||||
|
|
||||||
|
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
|
||||||
|
source /home/vagrant/.rvm/scripts/rvm
|
||||||
|
|
||||||
|
# Install Ruby
|
||||||
|
rvm reinstall ruby-$RUBY_VERSION --disable-binary
|
||||||
|
|
||||||
|
# Configure database
|
||||||
|
sudo -u postgres createuser -U postgres vagrant -s
|
||||||
|
sudo -u postgres createdb -U postgres mastodon_development
|
||||||
|
|
||||||
|
# Install gems and node modules
|
||||||
|
gem install bundler foreman
|
||||||
bundle install
|
bundle install
|
||||||
|
|
||||||
echo '[treehouse-vagrant] yarn install'
|
|
||||||
yarn add webpack
|
|
||||||
git restore package.json yarn.lock
|
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
echo '[treehouse-vagrant] bundle db'
|
# Build Mastodon
|
||||||
bundle exec rake db:setup || exit
|
export RAILS_ENV=development
|
||||||
|
export $(cat ".env.vagrant" | xargs)
|
||||||
|
bundle exec rails db:setup
|
||||||
|
|
||||||
echo '[treehouse-vagrant] bundle assets'
|
# Configure automatic loading of environment variable
|
||||||
bundle exec rake assets:precompile || exit
|
echo 'export RAILS_ENV=development' >> ~/.bash_profile
|
||||||
|
echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
|
||||||
echo '[treehouse-vagrant] foreman start'
|
|
||||||
foreman start
|
|
||||||
|
|
||||||
SCRIPT
|
SCRIPT
|
||||||
|
|
||||||
# TODO: allow `vagrant up`
|
$start = <<SCRIPT
|
||||||
# this requires persistent storage
|
|
||||||
# i.e., not saving data, assets, etc to /vagrant
|
|
||||||
#$start = <<SCRIPT
|
|
||||||
#
|
|
||||||
#export NODE_ENV=development
|
|
||||||
#export RAILS_ENV=development
|
|
||||||
#cd /vagrant
|
|
||||||
#pg_ctl -D data/postgres start --silent
|
|
||||||
#redis-server ./redis-dev.conf
|
|
||||||
#foreman start
|
|
||||||
#
|
|
||||||
#SCRIPT
|
|
||||||
|
|
||||||
Vagrant.configure("2") do |config|
|
echo 'To start server'
|
||||||
|
echo ' $ vagrant ssh -c "cd /vagrant && foreman start"'
|
||||||
|
|
||||||
config.vagrant.plugins = "vagrant-libvirt"
|
SCRIPT
|
||||||
|
|
||||||
config.vm.box = "archlinux/archlinux"
|
VAGRANTFILE_API_VERSION = "2"
|
||||||
config.vm.hostname = "mastodon.local"
|
|
||||||
|
|
||||||
# vagrant ssh -- -L 3000:localhost:3000 (:
|
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||||
#config.vm.network :forwarded_port, guest: 3000, host: 3000
|
|
||||||
|
|
||||||
config.vm.synced_folder ".", "/vagrant",
|
config.vm.box = "ubuntu/bionic64"
|
||||||
type: "rsync",
|
|
||||||
rsync__args: ["--verbose", "--archive", "--delete", "-z"],
|
|
||||||
rsync__exclide: ".git/"
|
|
||||||
|
|
||||||
config.vm.provision :shell, inline: $provision, privileged: false
|
config.vm.provider :virtualbox do |vb|
|
||||||
#config.vm.provision :shell, inline: $start, run: 'always', privileged: false
|
vb.name = "mastodon"
|
||||||
|
vb.customize ["modifyvm", :id, "--memory", "4096"]
|
||||||
|
# Increase the number of CPUs. Uncomment and adjust to
|
||||||
|
# increase performance
|
||||||
|
# vb.customize ["modifyvm", :id, "--cpus", "3"]
|
||||||
|
|
||||||
|
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
|
||||||
|
# https://github.com/mitchellh/vagrant/issues/1172
|
||||||
|
vb.customize ["modifyvm", :id, "--natdnsproxy1", "off"]
|
||||||
|
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "off"]
|
||||||
|
|
||||||
|
# Use "virtio" network interfaces for better performance.
|
||||||
|
vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
|
||||||
|
vb.customize ["modifyvm", :id, "--nictype2", "virtio"]
|
||||||
|
|
||||||
config.vm.provider :libvirt do |libvirt|
|
|
||||||
libvirt.driver = "kvm"
|
|
||||||
libvirt.memory = 4096
|
|
||||||
libvirt.cpus = 4
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This uses the vagrant-hostsupdater plugin, and lets you
|
||||||
|
# access the development site at http://mastodon.local.
|
||||||
|
# If you change it, also change it in .env.vagrant before provisioning
|
||||||
|
# the vagrant server to update the development build.
|
||||||
|
#
|
||||||
|
# To install:
|
||||||
|
# $ vagrant plugin install vagrant-hostsupdater
|
||||||
|
config.vm.hostname = "mastodon.local"
|
||||||
|
|
||||||
|
if defined?(VagrantPlugins::HostsUpdater)
|
||||||
|
config.vm.network :private_network, ip: "192.168.42.42", nictype: "virtio"
|
||||||
|
config.hostsupdater.remove_on_suspend = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if config.vm.networks.any? { |type, options| type == :private_network }
|
||||||
|
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp', 'actimeo=1']
|
||||||
|
else
|
||||||
|
config.vm.synced_folder ".", "/vagrant"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080
|
||||||
|
config.vm.network :forwarded_port, guest: 3000, host: 3000
|
||||||
|
config.vm.network :forwarded_port, guest: 4000, host: 4000
|
||||||
|
config.vm.network :forwarded_port, guest: 8080, host: 8080
|
||||||
|
|
||||||
|
# Full provisioning script, only runs on first 'vagrant up' or with 'vagrant provision'
|
||||||
|
config.vm.provision :shell, inline: $provision, privileged: false
|
||||||
|
|
||||||
|
# Start up script, runs on every 'vagrant up'
|
||||||
|
config.vm.provision :shell, inline: $start, run: 'always', privileged: false
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -65,7 +65,8 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
poll: status_params[:poll],
|
poll: status_params[:poll],
|
||||||
content_type: status_params[:content_type],
|
content_type: status_params[:content_type],
|
||||||
idempotency: request.headers['Idempotency-Key'],
|
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
|
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||||
|
@ -129,6 +130,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
:visibility,
|
:visibility,
|
||||||
:language,
|
:language,
|
||||||
:scheduled_at,
|
:scheduled_at,
|
||||||
|
:quote_id,
|
||||||
:content_type,
|
:content_type,
|
||||||
media_ids: [],
|
media_ids: [],
|
||||||
poll: [
|
poll: [
|
||||||
|
|
|
@ -24,6 +24,7 @@ module ContextHelper
|
||||||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
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' },
|
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' },
|
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||||
|
quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' },
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def full_context
|
def full_context
|
||||||
|
|
|
@ -15,7 +15,17 @@ module FormattingHelper
|
||||||
module_function :extract_status_plain_text
|
module_function :extract_status_plain_text
|
||||||
|
|
||||||
def status_content_format(status)
|
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
|
end
|
||||||
|
|
||||||
def rss_status_content_format(status)
|
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_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||||
|
|
||||||
|
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
||||||
|
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
|
@ -134,6 +137,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() {
|
export function resetCompose() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_RESET,
|
type: COMPOSE_RESET,
|
||||||
|
@ -187,6 +209,7 @@ export function submitCompose(routerHistory) {
|
||||||
status,
|
status,
|
||||||
content_type: getState().getIn(['compose', 'content_type']),
|
content_type: getState().getIn(['compose', 'content_type']),
|
||||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
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_ids: media.map(item => item.get('id')),
|
||||||
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
|
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
|
||||||
spoiler_text: spoilerText,
|
spoiler_text: spoilerText,
|
||||||
|
|
|
@ -74,6 +74,8 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
||||||
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
||||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||||
normalStatus.hidden = normalOldStatus.get('hidden');
|
normalStatus.hidden = normalOldStatus.get('hidden');
|
||||||
|
normalStatus.quote = normalOldStatus.get('quote');
|
||||||
|
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
|
||||||
} else {
|
} else {
|
||||||
const spoilerText = normalStatus.spoiler_text || '';
|
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');
|
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.contentHtml = emojify(normalStatus.content, emojiMap);
|
||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
||||||
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
|
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;
|
return normalStatus;
|
||||||
|
|
|
@ -69,6 +69,7 @@ class Status extends ImmutablePureComponent {
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
|
onQuote: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
onReblog: PropTypes.func,
|
onReblog: PropTypes.func,
|
||||||
onBookmark: PropTypes.func,
|
onBookmark: PropTypes.func,
|
||||||
|
@ -687,7 +688,7 @@ class Status extends ImmutablePureComponent {
|
||||||
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
|
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
|
||||||
background = attachments.getIn([0, 'preview_url']);
|
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(
|
media.push(
|
||||||
<Card
|
<Card
|
||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
|
|
@ -25,6 +25,7 @@ const messages = defineMessages({
|
||||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
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' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
|
@ -58,6 +59,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
onReblog: PropTypes.func,
|
onReblog: PropTypes.func,
|
||||||
|
onQuote: PropTypes.func,
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
onDirect: PropTypes.func,
|
onDirect: PropTypes.func,
|
||||||
onMention: 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) => {
|
handleBookmarkClick = (e) => {
|
||||||
this.props.onBookmark(this.props.status, e);
|
this.props.onBookmark(this.props.status, e);
|
||||||
}
|
}
|
||||||
|
@ -307,6 +320,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
obfuscateCount
|
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={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} />
|
<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}
|
{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} />
|
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||||
|
|
|
@ -275,6 +275,34 @@ export default class StatusContent extends React.PureComponent {
|
||||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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"
|
||||||
|
dangerouslySetInnerHTML={quoteStatusDisplayName} />
|
||||||
|
</span>
|
||||||
|
</bdi>
|
||||||
|
<div dangerouslySetInnerHTML={quoteStatusContent} />
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (status.get('spoiler_text').length > 0) {
|
if (status.get('spoiler_text').length > 0) {
|
||||||
let mentionsPlaceholder = '';
|
let mentionsPlaceholder = '';
|
||||||
|
|
||||||
|
@ -340,6 +368,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
||||||
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
|
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
|
||||||
|
{quote}
|
||||||
<div
|
<div
|
||||||
ref={this.setContentsRef}
|
ref={this.setContentsRef}
|
||||||
key={`contents-${tagLinks}`}
|
key={`contents-${tagLinks}`}
|
||||||
|
@ -365,6 +394,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
onMouseUp={this.handleMouseUp}
|
onMouseUp={this.handleMouseUp}
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
>
|
>
|
||||||
|
{quote}
|
||||||
<div
|
<div
|
||||||
ref={this.setContentsRef}
|
ref={this.setContentsRef}
|
||||||
key={`contents-${tagLinks}-${rewriteMentions}`}
|
key={`contents-${tagLinks}-${rewriteMentions}`}
|
||||||
|
@ -385,6 +415,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
className='status__content'
|
className='status__content'
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
>
|
>
|
||||||
|
{quote}
|
||||||
<div
|
<div
|
||||||
ref={this.setContentsRef}
|
ref={this.setContentsRef}
|
||||||
key={`contents-${tagLinks}`}
|
key={`contents-${tagLinks}`}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { List as ImmutableList } from 'immutable';
|
||||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
|
quoteCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
directCompose,
|
directCompose,
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
|
@ -50,6 +51,8 @@ const messages = defineMessages({
|
||||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
|
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying 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' },
|
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
|
||||||
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
|
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
|
||||||
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
|
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
|
||||||
|
@ -111,6 +114,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) {
|
onModalReblog (status, privacy) {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||||
|
import QuoteIndicatorContainer from '../containers/quote_indicator_container';
|
||||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
@ -309,6 +310,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
<WarningContainer />
|
<WarningContainer />
|
||||||
|
|
||||||
<ReplyIndicatorContainer />
|
<ReplyIndicatorContainer />
|
||||||
|
<QuoteIndicatorContainer />
|
||||||
|
|
||||||
<div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}>
|
<div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}>
|
||||||
<AutosuggestInput
|
<AutosuggestInput
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
// 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
// Components.
|
// Components.
|
||||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||||
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
import IconButton from 'flavours/glitch/components/icon_button';
|
import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
|
|
||||||
|
@ -58,6 +59,9 @@ class ReplyIndicator extends ImmutablePureComponent {
|
||||||
title={intl.formatMessage(messages.cancel)}
|
title={intl.formatMessage(messages.cancel)}
|
||||||
inverted
|
inverted
|
||||||
/>
|
/>
|
||||||
|
<Icon
|
||||||
|
className='quote-indicator__cancel icon-button inverted'
|
||||||
|
id='reply' />
|
||||||
{account && (
|
{account && (
|
||||||
<AccountContainer
|
<AccountContainer
|
||||||
id={account}
|
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' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
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' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
|
@ -52,6 +53,7 @@ class ActionBar extends React.PureComponent {
|
||||||
onReblog: PropTypes.func.isRequired,
|
onReblog: PropTypes.func.isRequired,
|
||||||
onFavourite: PropTypes.func.isRequired,
|
onFavourite: PropTypes.func.isRequired,
|
||||||
onBookmark: PropTypes.func.isRequired,
|
onBookmark: PropTypes.func.isRequired,
|
||||||
|
onQuote: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func,
|
onMute: PropTypes.func,
|
||||||
onMuteConversation: PropTypes.func,
|
onMuteConversation: PropTypes.func,
|
||||||
onBlock: PropTypes.func,
|
onBlock: PropTypes.func,
|
||||||
|
@ -81,6 +83,10 @@ class ActionBar extends React.PureComponent {
|
||||||
this.props.onBookmark(this.props.status, e);
|
this.props.onBookmark(this.props.status, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleQuoteClick = () => {
|
||||||
|
this.props.onQuote(this.props.status);
|
||||||
|
}
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history);
|
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__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 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={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>
|
<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}
|
{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>
|
<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>
|
||||||
|
|
|
@ -210,7 +210,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
mediaIcons.push('picture-o');
|
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')} />);
|
media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />);
|
||||||
mediaIcons.push('link');
|
mediaIcons.push('link');
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import DetailedStatus from '../components/detailed_status';
|
||||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
|
quoteCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
directCompose,
|
directCompose,
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
|
@ -33,6 +34,8 @@ import { showAlertForError } from 'flavours/glitch/actions/alerts';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
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' },
|
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.' },
|
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' },
|
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) {
|
onModalReblog (status, privacy) {
|
||||||
dispatch(reblog(status, privacy));
|
dispatch(reblog(status, privacy));
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
} from 'flavours/glitch/actions/interactions';
|
} from 'flavours/glitch/actions/interactions';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
|
quoteCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
directCompose,
|
directCompose,
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
|
@ -321,6 +322,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) => {
|
handleModalReblog = (status, privacy) => {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
@ -679,6 +695,7 @@ class Status extends ImmutablePureComponent {
|
||||||
onFavourite={this.handleFavouriteClick}
|
onFavourite={this.handleFavouriteClick}
|
||||||
onReblog={this.handleReblogClick}
|
onReblog={this.handleReblogClick}
|
||||||
onBookmark={this.handleBookmarkClick}
|
onBookmark={this.handleBookmarkClick}
|
||||||
|
onQuote={this.handleQuoteClick}
|
||||||
onDelete={this.handleDeleteClick}
|
onDelete={this.handleDeleteClick}
|
||||||
onEdit={this.handleEditClick}
|
onEdit={this.handleEditClick}
|
||||||
onDirect={this.handleDirectClick}
|
onDirect={this.handleDirectClick}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {
|
||||||
COMPOSE_REPLY,
|
COMPOSE_REPLY,
|
||||||
COMPOSE_REPLY_CANCEL,
|
COMPOSE_REPLY_CANCEL,
|
||||||
COMPOSE_DIRECT,
|
COMPOSE_DIRECT,
|
||||||
|
COMPOSE_QUOTE,
|
||||||
|
COMPOSE_QUOTE_CANCEL,
|
||||||
COMPOSE_MENTION,
|
COMPOSE_MENTION,
|
||||||
COMPOSE_SUBMIT_REQUEST,
|
COMPOSE_SUBMIT_REQUEST,
|
||||||
COMPOSE_SUBMIT_SUCCESS,
|
COMPOSE_SUBMIT_SUCCESS,
|
||||||
|
@ -85,6 +87,7 @@ const initialState = ImmutableMap({
|
||||||
caretPosition: null,
|
caretPosition: null,
|
||||||
preselectDate: null,
|
preselectDate: null,
|
||||||
in_reply_to: null,
|
in_reply_to: null,
|
||||||
|
quote_id: null,
|
||||||
is_submitting: false,
|
is_submitting: false,
|
||||||
is_uploading: false,
|
is_uploading: false,
|
||||||
is_changing_upload: false,
|
is_changing_upload: false,
|
||||||
|
@ -173,6 +176,7 @@ function clearAll(state) {
|
||||||
map.set('is_submitting', false);
|
map.set('is_submitting', false);
|
||||||
map.set('is_changing_upload', false);
|
map.set('is_changing_upload', false);
|
||||||
map.set('in_reply_to', null);
|
map.set('in_reply_to', null);
|
||||||
|
map.set('quote_id', null);
|
||||||
map.update(
|
map.update(
|
||||||
'advanced_options',
|
'advanced_options',
|
||||||
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
|
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
|
||||||
|
@ -410,6 +414,7 @@ export default function compose(state = initialState, action) {
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('id', null);
|
map.set('id', null);
|
||||||
map.set('in_reply_to', action.status.get('id'));
|
map.set('in_reply_to', action.status.get('id'));
|
||||||
|
map.set('quote_id', null);
|
||||||
map.set('text', statusToTextMentions(state, action.status));
|
map.set('text', statusToTextMentions(state, action.status));
|
||||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||||
map.update(
|
map.update(
|
||||||
|
@ -437,11 +442,29 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
case COMPOSE_QUOTE:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set('id', null);
|
||||||
|
map.set('in_reply_to', null);
|
||||||
|
map.set('quote_id', action.status.get('id'));
|
||||||
|
map.set('text', '');
|
||||||
|
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());
|
||||||
|
});
|
||||||
case COMPOSE_REPLY_CANCEL:
|
case COMPOSE_REPLY_CANCEL:
|
||||||
state = state.setIn(['advanced_options', 'threaded_mode'], false);
|
state = state.setIn(['advanced_options', 'threaded_mode'], false);
|
||||||
|
case COMPOSE_QUOTE_CANCEL:
|
||||||
case COMPOSE_RESET:
|
case COMPOSE_RESET:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('in_reply_to', null);
|
map.set('in_reply_to', null);
|
||||||
|
map.set('quote_id', null);
|
||||||
if (defaultContentType) map.set('content_type', defaultContentType);
|
if (defaultContentType) map.set('content_type', defaultContentType);
|
||||||
map.set('text', '');
|
map.set('text', '');
|
||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
|
|
|
@ -123,6 +123,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-indicator,
|
||||||
.reply-indicator {
|
.reply-indicator {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -133,6 +134,7 @@
|
||||||
flex: 0 2 auto;
|
flex: 0 2 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-indicator__header,
|
||||||
.reply-indicator__header {
|
.reply-indicator__header {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -140,11 +142,13 @@
|
||||||
& > .account.small { color: $inverted-text-color; }
|
& > .account.small { color: $inverted-text-color; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-indicator__cancel,
|
||||||
.reply-indicator__cancel {
|
.reply-indicator__cancel {
|
||||||
float: right;
|
float: right;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-indicator__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
|
|
|
@ -77,6 +77,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status__quote {
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__quote,
|
||||||
.status__content__text,
|
.status__content__text,
|
||||||
.e-content {
|
.e-content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -123,6 +128,11 @@
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i[role=img] {
|
||||||
|
font-style: normal;
|
||||||
|
padding-right: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
sub {
|
sub {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
vertical-align: sub;
|
vertical-align: sub;
|
||||||
|
@ -285,28 +295,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@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 {
|
.focusable {
|
||||||
&:focus,
|
&:focus {
|
||||||
&:hover {
|
outline: 0;
|
||||||
@include focusable;
|
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%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -691,6 +696,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
a.status__display-name,
|
a.status__display-name,
|
||||||
|
.quote-indicator__display-name,
|
||||||
.reply-indicator__display-name,
|
.reply-indicator__display-name,
|
||||||
.detailed-status__display-name,
|
.detailed-status__display-name,
|
||||||
.account__display-name {
|
.account__display-name {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
.status__content a,
|
.status__content a,
|
||||||
.link-footer a,
|
.link-footer a,
|
||||||
|
.quote-indicator__content a,
|
||||||
.reply-indicator__content a,
|
.reply-indicator__content a,
|
||||||
.status__content__read-more-button {
|
.status__content__read-more-button {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
|
@ -257,6 +257,7 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the background colors of status__content__spoiler-link
|
// Change the background colors of status__content__spoiler-link
|
||||||
|
.quote-indicator__content .status__content__spoiler-link,
|
||||||
.reply-indicator__content .status__content__spoiler-link,
|
.reply-indicator__content .status__content__spoiler-link,
|
||||||
.status__content .status__content__spoiler-link {
|
.status__content .status__content__spoiler-link {
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
|
@ -662,6 +663,7 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-indicator,
|
||||||
.reply-indicator {
|
.reply-indicator {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
@ -673,6 +675,7 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__content,
|
.status__content,
|
||||||
|
.quote-indicator__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
a {
|
a {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
|
|
|
@ -79,6 +79,8 @@ class LinkFooter extends React.PureComponent {
|
||||||
{' '}
|
{' '}
|
||||||
<a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
|
<a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
|
||||||
{' · '}
|
{' · '}
|
||||||
|
<a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a>
|
||||||
|
{' · '}
|
||||||
<Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
|
<Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
|
||||||
{' · '}
|
{' · '}
|
||||||
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
||||||
|
|
|
@ -126,6 +126,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
conversation: conversation_from_uri(@object['conversation']),
|
conversation: conversation_from_uri(@object['conversation']),
|
||||||
media_attachment_ids: process_attachments.take(4).map(&:id),
|
media_attachment_ids: process_attachments.take(4).map(&:id),
|
||||||
poll: process_poll,
|
poll: process_poll,
|
||||||
|
quote: process_quote,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -426,4 +427,24 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
poll.reload
|
poll.reload
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def guess_quote_url
|
||||||
|
if @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
|
end
|
||||||
|
|
|
@ -14,6 +14,8 @@ module ActivityPub::CaseTransform
|
||||||
when String
|
when String
|
||||||
camel_lower_cache[value] ||= if value.start_with?('_:')
|
camel_lower_cache[value] ||= if value.start_with?('_:')
|
||||||
'_:' + value.gsub(/\A_:/, '').underscore.camelize(:lower)
|
'_:' + value.gsub(/\A_:/, '').underscore.camelize(:lower)
|
||||||
|
elsif value.start_with?('_')
|
||||||
|
value
|
||||||
else
|
else
|
||||||
value.underscore.camelize(:lower)
|
value.underscore.camelize(:lower)
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
# edited_at :datetime
|
# edited_at :datetime
|
||||||
# trendable :boolean
|
# trendable :boolean
|
||||||
# ordered_media_attachment_ids :bigint(8) is an Array
|
# ordered_media_attachment_ids :bigint(8) is an Array
|
||||||
|
# quote_id :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
class Status < ApplicationRecord
|
class Status < ApplicationRecord
|
||||||
|
@ -61,6 +62,7 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
|
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 :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 :favourites, inverse_of: :status, dependent: :destroy
|
||||||
has_many :bookmarks, inverse_of: :status, dependent: :destroy
|
has_many :bookmarks, inverse_of: :status, dependent: :destroy
|
||||||
|
@ -70,6 +72,7 @@ class Status < ApplicationRecord
|
||||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
|
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
|
||||||
has_many :media_attachments, dependent: :nullify
|
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 :tags
|
||||||
has_and_belongs_to_many :preview_cards
|
has_and_belongs_to_many :preview_cards
|
||||||
|
@ -86,6 +89,7 @@ class Status < ApplicationRecord
|
||||||
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
|
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
|
||||||
validates :visibility, exclusion: { in: %w(direct limited) }, 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 :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
|
accepts_nested_attributes_for :poll
|
||||||
|
|
||||||
|
@ -134,6 +138,17 @@ class Status < ApplicationRecord
|
||||||
account: [:account_stat, :user],
|
account: [:account_stat, :user],
|
||||||
active_mentions: { account: :account_stat },
|
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 }
|
thread: { account: :account_stat }
|
||||||
|
|
||||||
delegate :domain, to: :account, prefix: true
|
delegate :domain, to: :account, prefix: true
|
||||||
|
@ -195,6 +210,14 @@ class Status < ApplicationRecord
|
||||||
!reblog_of_id.nil?
|
!reblog_of_id.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote?
|
||||||
|
!quote_id.nil? && quote
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote_visibility
|
||||||
|
quote&.visibility
|
||||||
|
end
|
||||||
|
|
||||||
def within_realtime_window?
|
def within_realtime_window?
|
||||||
created_at >= REAL_TIME_WINDOW.ago
|
created_at >= REAL_TIME_WINDOW.ago
|
||||||
end
|
end
|
||||||
|
@ -259,7 +282,7 @@ class Status < ApplicationRecord
|
||||||
fields = [spoiler_text, text]
|
fields = [spoiler_text, text]
|
||||||
fields += preloadable_poll.options unless preloadable_poll.nil?
|
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
|
end
|
||||||
|
|
||||||
def ordered_media_attachments
|
def ordered_media_attachments
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
include FormattingHelper
|
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,
|
attributes :id, :type, :summary,
|
||||||
:in_reply_to, :published, :url,
|
:in_reply_to, :published, :url,
|
||||||
|
@ -11,6 +11,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
:atom_uri, :in_reply_to_atom_uri,
|
:atom_uri, :in_reply_to_atom_uri,
|
||||||
:conversation
|
:conversation
|
||||||
|
|
||||||
|
attribute :quote_uri, if: -> { object.quote? }
|
||||||
|
|
||||||
attribute :content
|
attribute :content
|
||||||
attribute :content_map, if: :language?
|
attribute :content_map, if: :language?
|
||||||
attribute :updated, if: :edited?
|
attribute :updated, if: :edited?
|
||||||
|
@ -149,6 +151,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote_uri
|
||||||
|
ActivityPub::TagManager.instance.uri_for(object.quote) if object.quote?
|
||||||
|
end
|
||||||
|
|
||||||
def local?
|
def local?
|
||||||
object.account.local?
|
object.account.local?
|
||||||
end
|
end
|
||||||
|
|
|
@ -184,3 +184,13 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
end
|
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
|
||||||
|
|
|
@ -74,7 +74,7 @@ class FetchLinkCardService < BaseService
|
||||||
@status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
|
@status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
|
||||||
else
|
else
|
||||||
document = Nokogiri::HTML(@status.text)
|
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)
|
links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,7 @@ class PostStatusService < BaseService
|
||||||
# @option [Doorkeeper::Application] :application
|
# @option [Doorkeeper::Application] :application
|
||||||
# @option [String] :idempotency Optional idempotency key
|
# @option [String] :idempotency Optional idempotency key
|
||||||
# @option [Boolean] :with_rate_limit
|
# @option [Boolean] :with_rate_limit
|
||||||
|
# @option [String] :quote_id
|
||||||
# @return [Status]
|
# @return [Status]
|
||||||
def call(account, options = {})
|
def call(account, options = {})
|
||||||
@account = account
|
@account = account
|
||||||
|
@ -179,6 +180,7 @@ class PostStatusService < BaseService
|
||||||
application: @options[:application],
|
application: @options[:application],
|
||||||
content_type: @options[:content_type] || @account.user&.setting_default_content_type,
|
content_type: @options[:content_type] || @account.user&.setting_default_content_type,
|
||||||
rate_limit: @options[:with_rate_limit],
|
rate_limit: @options[:with_rate_limit],
|
||||||
|
quote_id: @options[:quote_id],
|
||||||
}.compact
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
|
|
||||||
= account_action_button(status.account)
|
= 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?) }<
|
.status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
||||||
- if status.spoiler_text?
|
- if status.spoiler_text?
|
||||||
%p<
|
%p<
|
||||||
|
|
|
@ -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,6 +27,10 @@
|
||||||
%span.display-name__account
|
%span.display-name__account
|
||||||
= acct(status.account)
|
= acct(status.account)
|
||||||
= fa_icon('lock') if status.account.locked?
|
= 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?) }<
|
.status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
||||||
- if status.spoiler_text?
|
- if status.spoiler_text?
|
||||||
%p<
|
%p<
|
||||||
|
|
|
@ -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
|
|
@ -943,6 +943,7 @@ ActiveRecord::Schema.define(version: 2022_11_04_133904) do
|
||||||
t.datetime "edited_at"
|
t.datetime "edited_at"
|
||||||
t.boolean "trendable"
|
t.boolean "trendable"
|
||||||
t.bigint "ordered_media_attachment_ids", array: true
|
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", "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 ["account_id"], name: "index_statuses_on_account_id"
|
||||||
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
|
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
|
||||||
|
@ -950,6 +951,7 @@ ActiveRecord::Schema.define(version: 2022_11_04_133904) 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 ["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_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 ["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 ["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)"
|
t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,8 +66,16 @@ services:
|
||||||
healthcheck:
|
healthcheck:
|
||||||
# prettier-ignore
|
# prettier-ignore
|
||||||
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1']
|
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1']
|
||||||
ports:
|
expose:
|
||||||
- '127.0.0.1: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:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
|
@ -87,8 +95,16 @@ services:
|
||||||
healthcheck:
|
healthcheck:
|
||||||
# prettier-ignore
|
# prettier-ignore
|
||||||
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1']
|
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1']
|
||||||
ports:
|
expose:
|
||||||
- '127.0.0.1:4000:4000'
|
- 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:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
|
|
|
@ -33,11 +33,11 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def repository
|
def repository
|
||||||
ENV.fetch('GIT_REPOSITORY', false) || ENV.fetch('GITHUB_REPOSITORY', false) || 'treehouse/mastodon'
|
ENV.fetch('GITHUB_REPOSITORY', 'glitch-soc/mastodon')
|
||||||
end
|
end
|
||||||
|
|
||||||
def source_base_url
|
def source_base_url
|
||||||
ENV.fetch('SOURCE_BASE_URL', "https://gitea.treehouse.systems/#{repository}")
|
ENV.fetch('SOURCE_BASE_URL', "https://github.com/#{repository}")
|
||||||
end
|
end
|
||||||
|
|
||||||
# specify git tag or commit hash here
|
# specify git tag or commit hash here
|
||||||
|
@ -46,13 +46,8 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def source_url
|
def source_url
|
||||||
if source_tag && source_base_url =~ /gitea/
|
if source_tag
|
||||||
suffix = if !str[/\H/]
|
"#{source_base_url}/tree/#{source_tag}"
|
||||||
"commit/#{source_tag}"
|
|
||||||
else
|
|
||||||
"branch/#{source_tag}"
|
|
||||||
end
|
|
||||||
"#{source_base_url}/#{suffix}"
|
|
||||||
else
|
else
|
||||||
source_base_url
|
source_base_url
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,6 +31,7 @@ class Sanitize
|
||||||
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
|
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
|
||||||
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
|
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
|
||||||
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
|
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
|
||||||
|
next true if /^quote-inline$/.match?(e) # quote inline classes
|
||||||
end
|
end
|
||||||
|
|
||||||
node['class'] = class_list.join(' ')
|
node['class'] = class_list.join(' ')
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
# 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
|
|
|
@ -36,13 +36,12 @@
|
||||||
"@rails/ujs": "^6.1.7",
|
"@rails/ujs": "^6.1.7",
|
||||||
"abortcontroller-polyfill": "^1.7.5",
|
"abortcontroller-polyfill": "^1.7.5",
|
||||||
"array-includes": "^3.1.5",
|
"array-includes": "^3.1.5",
|
||||||
"arrow-key-navigation": "^1.2.0",
|
|
||||||
"atrament": "0.2.4",
|
"atrament": "0.2.4",
|
||||||
|
"arrow-key-navigation": "^1.2.0",
|
||||||
"autoprefixer": "^9.8.8",
|
"autoprefixer": "^9.8.8",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.1.3",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
|
||||||
"babel-plugin-preval": "^5.1.0",
|
"babel-plugin-preval": "^5.1.0",
|
||||||
"babel-plugin-react-intl": "^6.2.0",
|
"babel-plugin-react-intl": "^6.2.0",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
|
@ -174,6 +173,5 @@
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"bufferutil": "^4.0.7",
|
"bufferutil": "^4.0.7",
|
||||||
"utf-8-validate": "^5.0.10"
|
"utf-8-validate": "^5.0.10"
|
||||||
},
|
}
|
||||||
"packageManager": "yarn@3.3.0"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,8 +87,8 @@ describe InstancePresenter do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#source_url' do
|
describe '#source_url' do
|
||||||
it 'returns the default URL' do
|
it 'returns "https://github.com/glitch-soc/mastodon"' do
|
||||||
expect(instance_presenter.source_url).to eq('https://gitea.treehouse.systems/treehouse/mastodon')
|
expect(instance_presenter.source_url).to eq('https://github.com/glitch-soc/mastodon')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue