mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 16:28:59 +00:00
Compare commits
41 Commits
unify-time
...
images-in-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6839ee390f | ||
|
|
54c1f56c9a | ||
|
|
2a9805b987 | ||
|
|
126f929c39 | ||
|
|
da42bfadb5 | ||
|
|
6ad72728f6 | ||
|
|
64d9c016bd | ||
|
|
12e7c81dd8 | ||
|
|
16d0aed403 | ||
|
|
da9317fa56 | ||
|
|
be92babd00 | ||
|
|
e2dd576a1b | ||
|
|
8f2c91568c | ||
|
|
98eaa2aa27 | ||
|
|
42b8220632 | ||
|
|
a91d968cab | ||
|
|
646de92781 | ||
|
|
ae2b722f55 | ||
|
|
7aeb9168b0 | ||
|
|
f53ed108b0 | ||
|
|
285038972b | ||
|
|
e5563843a2 | ||
|
|
c972e1ee1f | ||
|
|
5e8d037e27 | ||
|
|
ed7dc1704d | ||
|
|
436ce03772 | ||
|
|
d821aba002 | ||
|
|
4ce1540094 | ||
|
|
67243bda31 | ||
|
|
8f991831b8 | ||
|
|
87efa38721 | ||
|
|
f7301bd5b9 | ||
|
|
099a3b4eac | ||
|
|
3d4e21f1ec | ||
|
|
68dca26a5d | ||
|
|
1fc096ec75 | ||
|
|
21c2bc119c | ||
|
|
d23293c762 | ||
|
|
138e5a0b1e | ||
|
|
79dacea962 | ||
|
|
4e6b5e7879 |
14
.gitattributes
vendored
Normal file
14
.gitattributes
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.eot -text
|
||||||
|
*.gif -text
|
||||||
|
*.gz -text
|
||||||
|
*.ico -text
|
||||||
|
*.jpg -text
|
||||||
|
*.mp3 -text
|
||||||
|
*.ogg -text
|
||||||
|
*.png -text
|
||||||
|
*.ttf -text
|
||||||
|
*.webm -text
|
||||||
|
*.woff -text
|
||||||
|
*.woff2 -text
|
||||||
|
spec/fixtures/requests/** -text !eol
|
||||||
1
Gemfile
1
Gemfile
@@ -20,6 +20,7 @@ gem 'paperclip-av-transcoder', '~> 0.6'
|
|||||||
|
|
||||||
gem 'addressable', '~> 2.5'
|
gem 'addressable', '~> 2.5'
|
||||||
gem 'bootsnap'
|
gem 'bootsnap'
|
||||||
|
gem 'browser'
|
||||||
gem 'cld3', '~> 3.1'
|
gem 'cld3', '~> 3.1'
|
||||||
gem 'devise', '~> 4.2'
|
gem 'devise', '~> 4.2'
|
||||||
gem 'devise-two-factor', '~> 3.0'
|
gem 'devise-two-factor', '~> 3.0'
|
||||||
|
|||||||
66
Gemfile.lock
66
Gemfile.lock
@@ -1,40 +1,40 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (5.1.1)
|
actioncable (5.1.2)
|
||||||
actionpack (= 5.1.1)
|
actionpack (= 5.1.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (~> 0.6.1)
|
websocket-driver (~> 0.6.1)
|
||||||
actionmailer (5.1.1)
|
actionmailer (5.1.2)
|
||||||
actionpack (= 5.1.1)
|
actionpack (= 5.1.2)
|
||||||
actionview (= 5.1.1)
|
actionview (= 5.1.2)
|
||||||
activejob (= 5.1.1)
|
activejob (= 5.1.2)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (5.1.1)
|
actionpack (5.1.2)
|
||||||
actionview (= 5.1.1)
|
actionview (= 5.1.2)
|
||||||
activesupport (= 5.1.1)
|
activesupport (= 5.1.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
rack-test (~> 0.6.3)
|
rack-test (~> 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||||
actionview (5.1.1)
|
actionview (5.1.2)
|
||||||
activesupport (= 5.1.1)
|
activesupport (= 5.1.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||||
active_record_query_trace (1.5.4)
|
active_record_query_trace (1.5.4)
|
||||||
activejob (5.1.1)
|
activejob (5.1.2)
|
||||||
activesupport (= 5.1.1)
|
activesupport (= 5.1.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (5.1.1)
|
activemodel (5.1.2)
|
||||||
activesupport (= 5.1.1)
|
activesupport (= 5.1.2)
|
||||||
activerecord (5.1.1)
|
activerecord (5.1.2)
|
||||||
activemodel (= 5.1.1)
|
activemodel (= 5.1.2)
|
||||||
activesupport (= 5.1.1)
|
activesupport (= 5.1.2)
|
||||||
arel (~> 8.0)
|
arel (~> 8.0)
|
||||||
activesupport (5.1.1)
|
activesupport (5.1.2)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (~> 0.7)
|
i18n (~> 0.7)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
@@ -70,6 +70,7 @@ GEM
|
|||||||
bootsnap (1.0.0)
|
bootsnap (1.0.0)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (3.6.2)
|
brakeman (3.6.2)
|
||||||
|
browser (2.4.0)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (5.5.1)
|
bullet (5.5.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@@ -297,17 +298,17 @@ GEM
|
|||||||
rack-test (0.6.3)
|
rack-test (0.6.3)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rack-timeout (0.4.2)
|
rack-timeout (0.4.2)
|
||||||
rails (5.1.1)
|
rails (5.1.2)
|
||||||
actioncable (= 5.1.1)
|
actioncable (= 5.1.2)
|
||||||
actionmailer (= 5.1.1)
|
actionmailer (= 5.1.2)
|
||||||
actionpack (= 5.1.1)
|
actionpack (= 5.1.2)
|
||||||
actionview (= 5.1.1)
|
actionview (= 5.1.2)
|
||||||
activejob (= 5.1.1)
|
activejob (= 5.1.2)
|
||||||
activemodel (= 5.1.1)
|
activemodel (= 5.1.2)
|
||||||
activerecord (= 5.1.1)
|
activerecord (= 5.1.2)
|
||||||
activesupport (= 5.1.1)
|
activesupport (= 5.1.2)
|
||||||
bundler (>= 1.3.0, < 2.0)
|
bundler (>= 1.3.0, < 2.0)
|
||||||
railties (= 5.1.1)
|
railties (= 5.1.2)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.2)
|
rails-controller-testing (1.0.2)
|
||||||
actionpack (~> 5.x, >= 5.0.1)
|
actionpack (~> 5.x, >= 5.0.1)
|
||||||
@@ -323,9 +324,9 @@ GEM
|
|||||||
railties (~> 5.0)
|
railties (~> 5.0)
|
||||||
rails-settings-cached (0.6.5)
|
rails-settings-cached (0.6.5)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (5.1.1)
|
railties (5.1.2)
|
||||||
actionpack (= 5.1.1)
|
actionpack (= 5.1.2)
|
||||||
activesupport (= 5.1.1)
|
activesupport (= 5.1.2)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.18.1, < 2.0)
|
thor (>= 0.18.1, < 2.0)
|
||||||
@@ -483,6 +484,7 @@ DEPENDENCIES
|
|||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman (~> 3.6)
|
brakeman (~> 3.6)
|
||||||
|
browser
|
||||||
bullet (~> 5.5)
|
bullet (~> 5.5)
|
||||||
bundler-audit (~> 0.5)
|
bundler-audit (~> 0.5)
|
||||||
capistrano (~> 3.8)
|
capistrano (~> 3.8)
|
||||||
|
|||||||
71
README.md
71
README.md
@@ -1,5 +1,70 @@
|
|||||||
Mastodon Glitch Edition
|
Mastodon
|
||||||
========
|
========
|
||||||
Now with automated deploys!
|
|
||||||
|
|
||||||
So here's the deal: we all work on this code, and then it runs on dev.glitch.social and anyone who uses that does so absolutely at their own risk. can you dig it?
|
[][travis]
|
||||||
|
[][code_climate]
|
||||||
|
|
||||||
|
[travis]: https://travis-ci.org/tootsuite/mastodon
|
||||||
|
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
|
||||||
|
|
||||||
|
Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
|
||||||
|
|
||||||
|
An alternative implementation of the GNU social project. Based on [ActivityStreams](https://en.wikipedia.org/wiki/Activity_Streams_(format)), [Webfinger](https://en.wikipedia.org/wiki/WebFinger), [PubsubHubbub](https://en.wikipedia.org/wiki/PubSubHubbub) and [Salmon](https://en.wikipedia.org/wiki/Salmon_(protocol)).
|
||||||
|
|
||||||
|
Click on the screenshot to watch a demo of the UI:
|
||||||
|
|
||||||
|
[][youtube_demo]
|
||||||
|
|
||||||
|
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
|
||||||
|
|
||||||
|
The project focus is a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
|
||||||
|
|
||||||
|
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
|
||||||
|
|
||||||
|
[patreon]: https://www.patreon.com/user?u=619786
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
|
||||||
|
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
|
||||||
|
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
|
||||||
|
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
|
||||||
|
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Fully interoperable with GNU social and any OStatus platform**
|
||||||
|
Whatever implements Atom feeds, ActivityStreams, Salmon, PubSubHubbub and Webfinger is part of the network
|
||||||
|
- **Real-time timeline updates**
|
||||||
|
See the updates of people you're following appear in real-time in the UI via WebSockets
|
||||||
|
- **Federated thread resolving**
|
||||||
|
If someone you follow replies to a user unknown to the server, the server fetches the full thread so you can view it without leaving the UI
|
||||||
|
- **Media attachments like images and WebM**
|
||||||
|
Upload and view images and WebM videos attached to the updates
|
||||||
|
- **OAuth2 and a straightforward REST API**
|
||||||
|
Mastodon acts as an OAuth2 provider so 3rd party apps can use the API, which is RESTful and simple
|
||||||
|
- **Background processing for long-running tasks**
|
||||||
|
Mastodon tries to be as fast and responsive as possible, so all long-running tasks that can be delegated to background processing, are
|
||||||
|
- **Deployable via Docker**
|
||||||
|
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Please follow the [development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) from the documentation repository.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
There are guides in the documentation repository for [deploying on various platforms](https://github.com/tootsuite/documentation#running-mastodon).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository. [Here are the guidelines for code contributions](CONTRIBUTING.md)
|
||||||
|
|
||||||
|
**IRC channel**: #mastodon on irc.freenode.net
|
||||||
|
|
||||||
|
## Extra credits
|
||||||
|
|
||||||
|
- The [Emoji One](https://github.com/Ranks/emojione) pack has been used for the emojis
|
||||||
|
- The error page image courtesy of [Dopatwo](https://www.youtube.com/user/dopatwo)
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class Api::V1::ReportsController < Api::BaseController
|
|||||||
status_ids: reported_status_ids,
|
status_ids: reported_status_ids,
|
||||||
comment: report_params[:comment]
|
comment: report_params[:comment]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
|
||||||
|
|
||||||
render :show
|
render :show
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
|
|||||||
include UserTrackingConcern
|
include UserTrackingConcern
|
||||||
|
|
||||||
helper_method :current_account
|
helper_method :current_account
|
||||||
|
helper_method :current_session
|
||||||
helper_method :single_user_mode?
|
helper_method :single_user_mode?
|
||||||
|
|
||||||
rescue_from ActionController::RoutingError, with: :not_found
|
rescue_from ActionController::RoutingError, with: :not_found
|
||||||
@@ -68,6 +69,10 @@ class ApplicationController < ActionController::Base
|
|||||||
@current_account ||= current_user.try(:account)
|
@current_account ||= current_user.try(:account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_session
|
||||||
|
@current_session ||= SessionActivation.find_by(session_id: session['auth_id'])
|
||||||
|
end
|
||||||
|
|
||||||
def cache_collection(raw, klass)
|
def cache_collection(raw, klass)
|
||||||
return raw unless klass.respond_to?(:with_includes)
|
return raw unless klass.respond_to?(:with_includes)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
|
|
||||||
before_action :check_enabled_registrations, only: [:new, :create]
|
before_action :check_enabled_registrations, only: [:new, :create]
|
||||||
before_action :configure_sign_up_params, only: [:create]
|
before_action :configure_sign_up_params, only: [:create]
|
||||||
|
before_action :set_sessions, only: [:edit, :update]
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
not_found
|
not_found
|
||||||
@@ -41,4 +42,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
def determine_layout
|
def determine_layout
|
||||||
%w(edit update).include?(action_name) ? 'admin' : 'auth'
|
%w(edit update).include?(action_name) ? 'admin' : 'auth'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_sessions
|
||||||
|
@sessions = current_user.session_activations
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class HomeController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@body_classes = 'app-body'
|
@body_classes = 'app-body'
|
||||||
@token = find_or_create_access_token.token
|
@token = current_session.token
|
||||||
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
||||||
@admin = Account.find_local(Setting.site_contact_username)
|
@admin = Account.find_local(Setting.site_contact_username)
|
||||||
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
|
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
|
||||||
@@ -16,14 +16,4 @@ class HomeController < ApplicationController
|
|||||||
def authenticate_user!
|
def authenticate_user!
|
||||||
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
|
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_access_token
|
|
||||||
Doorkeeper::AccessToken.find_or_create_for(
|
|
||||||
Doorkeeper::Application.where(superapp: true).first,
|
|
||||||
current_user.id,
|
|
||||||
Doorkeeper::OAuth::Scopes.from_string('read write follow'),
|
|
||||||
Doorkeeper.configuration.access_token_expires_in,
|
|
||||||
Doorkeeper.configuration.refresh_token_enabled?
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ module Settings
|
|||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :verify_otp_required, only: [:create]
|
before_action :verify_otp_required, only: [:create]
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
@confirmation = Form::TwoFactorConfirmation.new
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
current_user.otp_secret = User.generate_otp_secret(32)
|
current_user.otp_secret = User.generate_otp_secret(32)
|
||||||
@@ -16,13 +18,23 @@ module Settings
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
if current_user.validate_and_consume_otp!(confirmation_params[:code])
|
||||||
current_user.otp_required_for_login = false
|
current_user.otp_required_for_login = false
|
||||||
current_user.save!
|
current_user.save!
|
||||||
redirect_to settings_two_factor_authentication_path
|
redirect_to settings_two_factor_authentication_path
|
||||||
|
else
|
||||||
|
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
||||||
|
@confirmation = Form::TwoFactorConfirmation.new
|
||||||
|
render :show
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def confirmation_params
|
||||||
|
params.require(:form_two_factor_confirmation).permit(:code)
|
||||||
|
end
|
||||||
|
|
||||||
def verify_otp_required
|
def verify_otp_required
|
||||||
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
|
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,4 +41,16 @@ module SettingsHelper
|
|||||||
def hash_to_object(hash)
|
def hash_to_object(hash)
|
||||||
HashObject.new(hash)
|
HashObject.new(hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def session_device_icon(session)
|
||||||
|
device = session.detection.device
|
||||||
|
|
||||||
|
if device.mobile?
|
||||||
|
'mobile'
|
||||||
|
elsif device.tablet?
|
||||||
|
'tablet'
|
||||||
|
else
|
||||||
|
'desktop'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,4 +1,5 @@
|
|||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import { openModal, closeModal } from './modal';
|
||||||
|
|
||||||
export const REPORT_INIT = 'REPORT_INIT';
|
export const REPORT_INIT = 'REPORT_INIT';
|
||||||
export const REPORT_CANCEL = 'REPORT_CANCEL';
|
export const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||||
@@ -11,10 +12,14 @@ export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
|
|||||||
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
|
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
|
||||||
|
|
||||||
export function initReport(account, status) {
|
export function initReport(account, status) {
|
||||||
return {
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
type: REPORT_INIT,
|
type: REPORT_INIT,
|
||||||
account,
|
account,
|
||||||
status,
|
status,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('REPORT'));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,7 +45,10 @@ export function submitReport() {
|
|||||||
account_id: getState().getIn(['reports', 'new', 'account_id']),
|
account_id: getState().getIn(['reports', 'new', 'account_id']),
|
||||||
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
|
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
|
||||||
comment: getState().getIn(['reports', 'new', 'comment']),
|
comment: getState().getIn(['reports', 'new', 'comment']),
|
||||||
}).then(response => dispatch(submitReportSuccess(response.data))).catch(error => dispatch(submitReportFail(error)));
|
}).then(response => {
|
||||||
|
dispatch(closeModal());
|
||||||
|
dispatch(submitReportSuccess(response.data));
|
||||||
|
}).catch(error => dispatch(submitReportFail(error)));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
export default class ColumnCollapsable extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
icon: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string,
|
|
||||||
fullHeight: PropTypes.number.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onCollapse: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
collapsed: true,
|
|
||||||
animating: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleToggleCollapsed = () => {
|
|
||||||
const currentState = this.state.collapsed;
|
|
||||||
|
|
||||||
this.setState({ collapsed: !currentState, animating: true });
|
|
||||||
|
|
||||||
if (!currentState && this.props.onCollapse) {
|
|
||||||
this.props.onCollapse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTransitionEnd = () => {
|
|
||||||
this.setState({ animating: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { icon, title, fullHeight, children } = this.props;
|
|
||||||
const { collapsed, animating } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`column-collapsable ${collapsed ? 'collapsed' : ''}`} onTransitionEnd={this.handleTransitionEnd}>
|
|
||||||
<div role='button' tabIndex='0' title={`${title}`} className='column-collapsable__button column-icon' onClick={this.handleToggleCollapsed}>
|
|
||||||
<i className={`fa fa-${icon}`} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='column-collapsable__content' style={{ height: `${fullHeight}px` }}>
|
|
||||||
{(!collapsed || animating) && children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -132,7 +132,7 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
|
<div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
|
||||||
<div>
|
<div className='column-header__collapsible-inner'>
|
||||||
{(!collapsed || animating) && collapsedContent}
|
{(!collapsed || animating) && collapsedContent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default class IconButton extends React.PureComponent {
|
|||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
inverted: PropTypes.bool,
|
inverted: PropTypes.bool,
|
||||||
animate: PropTypes.bool,
|
animate: PropTypes.bool,
|
||||||
flip: PropTypes.bool,
|
|
||||||
overlay: PropTypes.bool,
|
overlay: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ export default class IconButton extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Motion defaultStyle={{ rotate: this.props.active ? (this.props.flip ? -180 : -360) : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? (this.props.flip ? -180 : -360) : 0, { stiffness: this.props.flip ? 60 : 120, damping: 7 }) : 0 }}>
|
<Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
||||||
{({ rotate }) =>
|
{({ rotate }) =>
|
||||||
<button
|
<button
|
||||||
aria-label={this.props.title}
|
aria-label={this.props.title}
|
||||||
|
|||||||
@@ -85,14 +85,24 @@ class Item extends React.PureComponent {
|
|||||||
let thumbnail = '';
|
let thumbnail = '';
|
||||||
|
|
||||||
if (attachment.get('type') === 'image') {
|
if (attachment.get('type') === 'image') {
|
||||||
|
const previewUrl = attachment.get('preview_url');
|
||||||
|
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
|
||||||
|
|
||||||
|
const originalUrl = attachment.get('url');
|
||||||
|
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
||||||
|
|
||||||
|
const srcSet = `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`;
|
||||||
|
const sizes = `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;
|
||||||
|
|
||||||
thumbnail = (
|
thumbnail = (
|
||||||
<a // eslint-disable-line jsx-a11y/anchor-has-content
|
<a
|
||||||
className='media-gallery__item-thumbnail'
|
className='media-gallery__item-thumbnail'
|
||||||
href={attachment.get('remote_url') || attachment.get('url')}
|
href={attachment.get('remote_url') || originalUrl}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
style={{ backgroundImage: `url(${attachment.get('preview_url')})` }}
|
>
|
||||||
/>
|
<img src={previewUrl} srcSet={srcSet} sizes={sizes} alt='' />
|
||||||
|
</a>
|
||||||
);
|
);
|
||||||
} else if (attachment.get('type') === 'gifv') {
|
} else if (attachment.get('type') === 'gifv') {
|
||||||
const autoPlay = !isIOS() && this.props.autoPlayGif;
|
const autoPlay = !isIOS() && this.props.autoPlayGif;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default class Permalink extends React.PureComponent {
|
|||||||
const { href, children, className, ...other } = this.props;
|
const { href, children, className, ...other } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={href} onClick={this.handleClick} {...other} className={'permalink ' + className}>
|
<a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,24 +3,19 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
import AvatarOverlay from './avatar_overlay';
|
import AvatarOverlay from './avatar_overlay';
|
||||||
|
import RelativeTimestamp from './relative_timestamp';
|
||||||
import DisplayName from './display_name';
|
import DisplayName from './display_name';
|
||||||
import MediaGallery from './media_gallery';
|
import MediaGallery from './media_gallery';
|
||||||
import VideoPlayer from './video_player';
|
import VideoPlayer from './video_player';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import IconButton from './icon_button';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import emojify from '../emoji';
|
import emojify from '../emoji';
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
||||||
|
|
||||||
const messages = defineMessages({
|
export default class Status extends ImmutablePureComponent {
|
||||||
collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
|
|
||||||
uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class StatusUnextended extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
@@ -42,14 +37,12 @@ class StatusUnextended extends ImmutablePureComponent {
|
|||||||
autoPlayGif: PropTypes.bool,
|
autoPlayGif: PropTypes.bool,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
intersectionObserverWrapper: PropTypes.object,
|
intersectionObserverWrapper: PropTypes.object,
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
isIntersecting: true, // assume intersecting until told otherwise
|
isIntersecting: true, // assume intersecting until told otherwise
|
||||||
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
||||||
isCollapsed: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
@@ -67,11 +60,7 @@ class StatusUnextended extends ImmutablePureComponent {
|
|||||||
updateOnStates = ['isExpanded']
|
updateOnStates = ['isExpanded']
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps, nextState) {
|
shouldComponentUpdate (nextProps, nextState) {
|
||||||
if (nextState.isCollapsed !== this.state.isCollapsed) {
|
if (!nextState.isIntersecting && nextState.isHidden) {
|
||||||
// If the collapsed state of the element has changed then we definitely
|
|
||||||
// need to re-update.
|
|
||||||
return true;
|
|
||||||
} else if (!nextState.isIntersecting && nextState.isHidden) {
|
|
||||||
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
|
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
|
||||||
// that either "isIntersecting" or "isHidden" matter, and then they're
|
// that either "isIntersecting" or "isHidden" matter, and then they're
|
||||||
// the only things that matter.
|
// the only things that matter.
|
||||||
@@ -85,13 +74,7 @@ class StatusUnextended extends ImmutablePureComponent {
|
|||||||
return super.shouldComponentUpdate(nextProps, nextState);
|
return super.shouldComponentUpdate(nextProps, nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps, prevState) {
|
|
||||||
if (prevState.isCollapsed !== this.state.isCollapsed) this.saveHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const node = this.node;
|
|
||||||
|
|
||||||
if (!this.props.intersectionObserverWrapper) {
|
if (!this.props.intersectionObserverWrapper) {
|
||||||
// TODO: enable IntersectionObserver optimization for notification statuses.
|
// TODO: enable IntersectionObserver optimization for notification statuses.
|
||||||
// These are managed in notifications/index.js rather than status_list.js
|
// These are managed in notifications/index.js rather than status_list.js
|
||||||
@@ -103,8 +86,6 @@ class StatusUnextended extends ImmutablePureComponent {
|
|||||||
this.handleIntersection
|
this.handleIntersection
|
||||||
);
|
);
|
||||||
|
|
||||||
if (node.clientHeight > 400 && !(this.props.status.get('reblog', null) !== null && typeof this.props.status.get('reblog') === 'object')) this.setState({ isCollapsed: true });
|
|
||||||
|
|
||||||
this.componentMounted = true;
|
this.componentMounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +124,7 @@ class StatusUnextended extends ImmutablePureComponent {
|
|||||||
|
|
||||||
saveHeight = () => {
|
saveHeight = () => {
|
||||||
if (this.node && this.node.children.length !== 0) {
|
if (this.node && this.node.children.length !== 0) {
|
||||||
this.height = this.node.clientHeight;
|
this.height = this.node.getBoundingClientRect().height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,18 +150,15 @@ class StatusUnextended extends ImmutablePureComponent {
|
|||||||
this.setState({ isExpanded: !this.state.isExpanded });
|
this.setState({ isExpanded: !this.state.isExpanded });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleCollapsedClick = () => {
|
|
||||||
this.setState({ isCollapsed: !this.state.isCollapsed });
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let media = null;
|
let media = null;
|
||||||
|
let mediaIcon = null;
|
||||||
let statusAvatar;
|
let statusAvatar;
|
||||||
|
|
||||||
// Exclude intersectionObserverWrapper from `other` variable
|
// Exclude intersectionObserverWrapper from `other` variable
|
||||||
// because intersection is managed in here.
|
// because intersection is managed in here.
|
||||||
const { status, account, intersectionObserverWrapper, intl, ...other } = this.props;
|
const { status, account, intersectionObserverWrapper, ...other } = this.props;
|
||||||
const { isExpanded, isIntersecting, isHidden, isCollapsed } = this.state;
|
const { isExpanded, isIntersecting, isHidden } = this.state;
|
||||||
|
|
||||||
if (status === null) {
|
if (status === null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -221,8 +199,10 @@ class StatusUnextended extends ImmutablePureComponent {
|
|||||||
|
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
|
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
|
||||||
|
mediaIcon = 'video-camera';
|
||||||
} else {
|
} else {
|
||||||
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
|
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
|
||||||
|
mediaIcon = 'picture-o';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,17 +213,9 @@ class StatusUnextended extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')} ${isCollapsed ? 'status-collapsed' : ''}`} data-id={status.get('id')} ref={this.handleRef}>
|
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}>
|
||||||
<div className='status__info'>
|
<div className='status__info'>
|
||||||
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||||
<IconButton
|
|
||||||
className='status__collapse-button'
|
|
||||||
animate flip
|
|
||||||
active={isCollapsed}
|
|
||||||
title={isCollapsed ? intl.formatMessage(messages.uncollapse) : intl.formatMessage(messages.collapse)}
|
|
||||||
icon='angle-double-up'
|
|
||||||
onClick={this.handleCollapsedClick}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
|
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
|
||||||
<div className='status__avatar'>
|
<div className='status__avatar'>
|
||||||
@@ -252,19 +224,17 @@ class StatusUnextended extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<DisplayName account={status.get('account')} />
|
<DisplayName account={status.get('account')} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusContent status={status} onClick={this.handleClick} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} onHeightUpdate={this.saveHeight} />
|
<StatusContent status={status} mediaIcon={mediaIcon} onClick={this.handleClick} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} onHeightUpdate={this.saveHeight}>
|
||||||
|
|
||||||
{isCollapsed ? null : media}
|
{media}
|
||||||
|
|
||||||
{isCollapsed ? null : <StatusActionBar status={status} account={account} {...other} />}
|
</StatusContent>
|
||||||
|
|
||||||
|
<StatusActionBar {...this.props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Status = injectIntl(StatusUnextended);
|
|
||||||
export default Status;
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import IconButton from './icon_button';
|
|||||||
import DropdownMenu from './dropdown_menu';
|
import DropdownMenu from './dropdown_menu';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import RelativeTimestamp from './relative_timestamp';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
@@ -88,7 +87,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
handleReport = () => {
|
handleReport = () => {
|
||||||
this.props.onReport(this.props.status);
|
this.props.onReport(this.props.status);
|
||||||
this.context.router.history.push('/report');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConversationMuteClick = () => {
|
handleConversationMuteClick = () => {
|
||||||
@@ -146,8 +144,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||||||
<div className='status__action-bar-dropdown'>
|
<div className='status__action-bar-dropdown'>
|
||||||
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
|
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
onExpandedToggle: PropTypes.func,
|
onExpandedToggle: PropTypes.func,
|
||||||
onHeightUpdate: PropTypes.func,
|
onHeightUpdate: PropTypes.func,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
|
mediaIcon: PropTypes.string,
|
||||||
|
children: PropTypes.element,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -107,7 +109,7 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status } = this.props;
|
const { status, children, mediaIcon } = this.props;
|
||||||
|
|
||||||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||||
|
|
||||||
@@ -128,15 +130,19 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
</Permalink>
|
</Permalink>
|
||||||
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
|
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
|
||||||
|
|
||||||
const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
|
const toggleText = hidden ? [<FormattedMessage id='status.show_more' defaultMessage='Show more' key='0' />, mediaIcon ? <i className={`fa fa-fw fa-${mediaIcon} status__content__spoiler-icon`} aria-hidden='true' key='1' /> : null] : [<FormattedMessage id='status.show_less' defaultMessage='Show less' key='0' />];
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
mentionsPlaceholder = <div>{mentionLinks}</div>;
|
mentionsPlaceholder = <div>{mentionLinks}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__content status__content--with-action' ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
<div className='status__content status__content--with-action' ref={this.setRef}>
|
||||||
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
<p
|
||||||
|
style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
|
||||||
|
onMouseDown={this.handleMouseDown}
|
||||||
|
onMouseUp={this.handleMouseUp}
|
||||||
|
>
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} />
|
<span dangerouslySetInnerHTML={spoilerContent} />
|
||||||
{' '}
|
{' '}
|
||||||
<button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</button>
|
<button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</button>
|
||||||
@@ -144,7 +150,15 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
|
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
||||||
<div className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
|
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
|
||||||
|
<div
|
||||||
|
style={directionStyle}
|
||||||
|
onMouseDown={this.handleMouseDown}
|
||||||
|
onMouseUp={this.handleMouseUp}
|
||||||
|
dangerouslySetInnerHTML={content}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.props.onClick) {
|
} else if (this.props.onClick) {
|
||||||
@@ -153,10 +167,14 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
className='status__content status__content--with-action'
|
className='status__content status__content--with-action'
|
||||||
style={directionStyle}
|
style={directionStyle}
|
||||||
|
>
|
||||||
|
<div
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onMouseUp={this.handleMouseUp}
|
onMouseUp={this.handleMouseUp}
|
||||||
dangerouslySetInnerHTML={content}
|
dangerouslySetInnerHTML={content}
|
||||||
/>
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
@@ -164,8 +182,10 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
className='status__content'
|
className='status__content'
|
||||||
style={directionStyle}
|
style={directionStyle}
|
||||||
dangerouslySetInnerHTML={content}
|
>
|
||||||
/>
|
<div dangerouslySetInnerHTML={content} />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import emojify from '../../../emoji';
|
|||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import Avatar from '../../../components/avatar';
|
import Motion from 'react-motion/lib/Motion';
|
||||||
|
import spring from 'react-motion/lib/spring';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -14,57 +16,61 @@ const messages = defineMessages({
|
|||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
const makeMapStateToProps = () => {
|
||||||
THIS IS A MESS BECAUSE EFFING MASTODON AND ITS EFFING HTML BIOS
|
const mapStateToProps = state => ({
|
||||||
INSTEAD OF JUST STORING EVERYTHING IN PLAIN EFFING TEXT ! ! ! !
|
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
|
||||||
BLANK LINES ALSO WON'T WORK BECAUSE RIGHT NOW MASTODON CONVERTS
|
});
|
||||||
THOSE INTO `<P>` ELEMENTS INSTEAD OF LEAVING IT AS `<BR><BR>` !
|
|
||||||
TL:DR; THIS IS LARGELY A HACK. WITH BETTER BACKEND STUFF WE CAN
|
|
||||||
IMPROVE THIS BY BETTER PREDICTING HOW THE METADATA WILL BE SENT
|
|
||||||
WHILE MAINTAINING BASIC PLAIN-TEXT PROCESSING. THE OTHER OPTION
|
|
||||||
IS TO TURN ALL BIOS INTO PLAIN-TEXT VIA A TREE-WALKER, AND THEN
|
|
||||||
PROCESS THE YAML AND LINKS AND EVERYTHING OURSELVES. THIS WOULD
|
|
||||||
BE INCREDIBLY COMPLICATED, AND IT WOULD BE A MILLION TIMES LESS
|
|
||||||
DIFFICULT IF MASTODON JUST GAVE US PLAIN-TEXT BIOS (WHICH QUITE
|
|
||||||
FRANKLY MAKES THE MOST SENSE SINCE THAT'S WHAT USERS PROVIDE IN
|
|
||||||
SETTINGS) TO BEGIN WITH AND LEFT ALL PROCESSING TO THE FRONTEND
|
|
||||||
TO HANDLE ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
|
|
||||||
ANYWAY I KNOW WHAT NEEDS TO BE DONE REGARDING BACKEND STUFF BUT
|
|
||||||
I'M NOT SMART ENOUGH TO FIGURE OUT HOW TO ACTUALLY IMPLEMENT IT
|
|
||||||
SO FEEL FREE TO @ ME IF YOU NEED MY IDEAS REGARDING THAT. UNTIL
|
|
||||||
THEN WE'LL JUST HAVE TO MAKE DO WITH THIS MESSY AND UNFORTUNATE
|
|
||||||
HACKING ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
|
|
||||||
|
|
||||||
with love,
|
return mapStateToProps;
|
||||||
@kibi@glitch.social <3
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NEW_LINE = /(?:^|\r?\n|<br\s*\/?>)/g;
|
|
||||||
const YAML_OPENER = /---/;
|
|
||||||
const YAML_CLOSER = /(?:---|\.\.\.)/;
|
|
||||||
const YAML_STRING = /(?:"(?:[^"\n]){1,32}"|'(?:[^'\n]){1,32}'|(?:[^'":\n]){1,32})/g;
|
|
||||||
const YAML_LINE = new RegExp('\\s*' + YAML_STRING.source + '\\s*:\\s*' + YAML_STRING.source + '\\s*', 'g');
|
|
||||||
const BIO_REGEX = new RegExp(NEW_LINE.source + '*' + YAML_OPENER.source + NEW_LINE.source + '+(?:' + YAML_LINE.source + NEW_LINE.source + '+){0,4}' + YAML_CLOSER.source + NEW_LINE.source + '*');
|
|
||||||
|
|
||||||
const processBio = (data) => {
|
|
||||||
let props = { text: data, metadata: [] };
|
|
||||||
let yaml = data.match(BIO_REGEX);
|
|
||||||
if (!yaml) return props;
|
|
||||||
else yaml = yaml[0];
|
|
||||||
let start = props.text.indexOf(yaml);
|
|
||||||
let end = start + yaml.length;
|
|
||||||
props.text = props.text.substr(0, start) + props.text.substr(end);
|
|
||||||
yaml = yaml.replace(NEW_LINE, '\n');
|
|
||||||
let metadata = (yaml ? yaml.match(YAML_LINE) : []) || [];
|
|
||||||
for (let i = 0; i < metadata.length; i++) {
|
|
||||||
let result = metadata[i].match(YAML_STRING);
|
|
||||||
if (result[0][0] === '"' || result[0][0] === '\'') result[0] = result[0].substr(1, result[0].length - 2);
|
|
||||||
if (result[1][0] === '"' || result[1][0] === '\'') result[0] = result[1].substr(1, result[1].length - 2);
|
|
||||||
props.metadata.push(result);
|
|
||||||
}
|
|
||||||
return props;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class Avatar extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
autoPlayGif: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isHovered: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMouseOver = () => {
|
||||||
|
if (this.state.isHovered) return;
|
||||||
|
this.setState({ isHovered: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOut = () => {
|
||||||
|
if (!this.state.isHovered) return;
|
||||||
|
this.setState({ isHovered: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, autoPlayGif } = this.props;
|
||||||
|
const { isHovered } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||||
|
{({ radius }) =>
|
||||||
|
<a // eslint-disable-line jsx-a11y/anchor-has-content
|
||||||
|
href={account.get('url')}
|
||||||
|
className='account__header__avatar'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener'
|
||||||
|
style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
|
||||||
|
onMouseOver={this.handleMouseOver}
|
||||||
|
onMouseOut={this.handleMouseOut}
|
||||||
|
onFocus={this.handleMouseOver}
|
||||||
|
onBlur={this.handleMouseOut}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Motion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@connect(makeMapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
export default class Header extends ImmutablePureComponent {
|
export default class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
@@ -73,6 +79,7 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
me: PropTypes.number.isRequired,
|
me: PropTypes.number.isRequired,
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
autoPlayGif: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -115,46 +122,22 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
lockedIcon = <i className='fa fa-lock' />;
|
lockedIcon = <i className='fa fa-lock' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const content = { __html: emojify(account.get('note')) };
|
||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||||
const { text, metadata } = processBio(account.get('note'));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account__header__wrapper'>
|
|
||||||
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
||||||
<div>
|
<div>
|
||||||
<a href={account.get('url')} target='_blank' rel='noopener'>
|
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
|
||||||
<span className='account__header__avatar'><Avatar src={account.get('avatar')} animate size={90} /></span>
|
|
||||||
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
|
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
|
||||||
</a>
|
|
||||||
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
|
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
|
||||||
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />
|
<div className='account__header__content' dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
{info}
|
{info}
|
||||||
{actionBtn}
|
{actionBtn}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{metadata.length && (
|
|
||||||
<div className='account__metadata'>
|
|
||||||
{(() => {
|
|
||||||
let data = [];
|
|
||||||
for (let i = 0; i < metadata.length; i++) {
|
|
||||||
data.push(
|
|
||||||
<div
|
|
||||||
className='account__metadata-item'
|
|
||||||
title={metadata[i][0] + ':' + metadata[i][1]}
|
|
||||||
key={i}
|
|
||||||
>
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: emojify(metadata[i][0]) }} />
|
|
||||||
<strong dangerouslySetInnerHTML={{ __html: emojify(metadata[i][1]) }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
) || null}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
handleReport = () => {
|
handleReport = () => {
|
||||||
this.props.onReport(this.props.account);
|
this.props.onReport(this.props.account);
|
||||||
this.context.router.history.push('/report');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMute = () => {
|
handleMute = () => {
|
||||||
|
|||||||
@@ -1,44 +1,144 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
import Column from '../../components/column';
|
||||||
|
import ColumnHeader from '../../components/column_header';
|
||||||
import {
|
import {
|
||||||
refreshCommunityTimeline,
|
refreshCommunityTimeline,
|
||||||
expandCommunityTimeline,
|
expandCommunityTimeline,
|
||||||
|
updateTimeline,
|
||||||
|
deleteFromTimelines,
|
||||||
|
connectTimeline,
|
||||||
|
disconnectTimeline,
|
||||||
} from '../../actions/timelines';
|
} from '../../actions/timelines';
|
||||||
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import Timeline from '../timeline';
|
import createStream from '../../stream';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
|
accessToken: state.getIn(['meta', 'access_token']),
|
||||||
|
});
|
||||||
|
|
||||||
|
@connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
export default class CommunityTimeline extends React.PureComponent {
|
export default class CommunityTimeline extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
columnId: PropTypes.string,
|
columnId: PropTypes.string,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
streamingAPIBaseURL: PropTypes.string.isRequired,
|
||||||
|
accessToken: PropTypes.string.isRequired,
|
||||||
|
hasUnread: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handlePin = () => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
|
||||||
|
if (columnId) {
|
||||||
|
dispatch(removeColumn(columnId));
|
||||||
|
} else {
|
||||||
|
dispatch(addColumn('COMMUNITY', {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMove = (dir) => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
dispatch(moveColumn(columnId, dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||||
|
|
||||||
|
dispatch(refreshCommunityTimeline());
|
||||||
|
|
||||||
|
if (typeof this._subscription !== 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
|
||||||
|
|
||||||
|
connected () {
|
||||||
|
dispatch(connectTimeline('community'));
|
||||||
|
},
|
||||||
|
|
||||||
|
reconnected () {
|
||||||
|
dispatch(connectTimeline('community'));
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnected () {
|
||||||
|
dispatch(disconnectTimeline('community'));
|
||||||
|
},
|
||||||
|
|
||||||
|
received (data) {
|
||||||
|
switch(data.event) {
|
||||||
|
case 'update':
|
||||||
|
dispatch(updateTimeline('community', JSON.parse(data.payload)));
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (typeof this._subscription !== 'undefined') {
|
||||||
|
this._subscription.close();
|
||||||
|
this._subscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
this.props.dispatch(expandCommunityTimeline());
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, columnId, multiColumn } = this.props;
|
const { intl, hasUnread, columnId, multiColumn } = this.props;
|
||||||
|
const pinned = !!columnId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Column ref={this.setRef}>
|
||||||
expand={expandCommunityTimeline}
|
<ColumnHeader
|
||||||
refresh={refreshCommunityTimeline}
|
|
||||||
streamId='public:local'
|
|
||||||
columnName='COMMUNITY'
|
|
||||||
columnId={columnId}
|
|
||||||
mulitColumn={multiColumn}
|
|
||||||
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
|
||||||
icon='users'
|
icon='users'
|
||||||
|
active={hasUnread}
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
settings={<ColumnSettingsContainer />}
|
onPin={this.handlePin}
|
||||||
scrollName='community_timeline'
|
onMove={this.handleMove}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
pinned={pinned}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
>
|
||||||
|
<ColumnSettingsContainer />
|
||||||
|
</ColumnHeader>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`community_timeline-${columnId}`}
|
||||||
timelineId='community'
|
timelineId='community'
|
||||||
|
loadMore={this.handleLoadMore}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||||
/>
|
/>
|
||||||
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ export default class ComposeForm extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
|
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
|
||||||
|
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||||
|
// Update the state to match the current text
|
||||||
|
this.props.onChange(this.autosuggestTextarea.textarea.value);
|
||||||
|
}
|
||||||
|
|
||||||
this.props.onSubmit();
|
this.props.onSubmit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
|
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
@@ -14,7 +15,9 @@ const messages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
||||||
loaded: state.getIn(['status_lists', 'favourites', 'loaded']),
|
loaded: state.getIn(['status_lists', 'favourites', 'loaded']),
|
||||||
|
me: state.getIn(['meta', 'me']),
|
||||||
});
|
});
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
@@ -23,8 +26,10 @@ export default class Favourites extends ImmutablePureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
statusIds: ImmutablePropTypes.list.isRequired,
|
||||||
loaded: PropTypes.bool,
|
loaded: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
me: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
|
|||||||
@@ -1,53 +1,138 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
import Column from '../../components/column';
|
||||||
|
import ColumnHeader from '../../components/column_header';
|
||||||
import {
|
import {
|
||||||
refreshHashtagTimeline,
|
refreshHashtagTimeline,
|
||||||
expandHashtagTimeline,
|
expandHashtagTimeline,
|
||||||
|
updateTimeline,
|
||||||
|
deleteFromTimelines,
|
||||||
} from '../../actions/timelines';
|
} from '../../actions/timelines';
|
||||||
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Timeline from '../timeline';
|
import createStream from '../../stream';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
|
accessToken: state.getIn(['meta', 'access_token']),
|
||||||
|
});
|
||||||
|
|
||||||
|
@connect(mapStateToProps)
|
||||||
export default class HashtagTimeline extends React.PureComponent {
|
export default class HashtagTimeline extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
columnId: PropTypes.string,
|
columnId: PropTypes.string,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
streamingAPIBaseURL: PropTypes.string.isRequired,
|
||||||
|
accessToken: PropTypes.string.isRequired,
|
||||||
|
hasUnread: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
handlePin = () => {
|
||||||
const id = this.props.params.id;
|
const { columnId, dispatch } = this.props;
|
||||||
this.expand = () => expandHashtagTimeline(id);
|
|
||||||
this.refresh = () => refreshHashtagTimeline(id);
|
if (columnId) {
|
||||||
|
dispatch(removeColumn(columnId));
|
||||||
|
} else {
|
||||||
|
dispatch(addColumn('HASHTAG', { id: this.props.params.id }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMove = (dir) => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
dispatch(moveColumn(columnId, dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
_subscribe (dispatch, id) {
|
||||||
|
const { streamingAPIBaseURL, accessToken } = this.props;
|
||||||
|
|
||||||
|
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
|
||||||
|
|
||||||
|
received (data) {
|
||||||
|
switch(data.event) {
|
||||||
|
case 'update':
|
||||||
|
dispatch(updateTimeline(`hashtag:${id}`, JSON.parse(data.payload)));
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_unsubscribe () {
|
||||||
|
if (typeof this.subscription !== 'undefined') {
|
||||||
|
this.subscription.close();
|
||||||
|
this.subscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { id } = this.props.params;
|
||||||
|
|
||||||
|
dispatch(refreshHashtagTimeline(id));
|
||||||
|
this._subscribe(dispatch, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (nextProps.params.id !== this.props.params.id) {
|
if (nextProps.params.id !== this.props.params.id) {
|
||||||
const id = nextProps.params.id;
|
this.props.dispatch(refreshHashtagTimeline(nextProps.params.id));
|
||||||
this.expand = () => expandHashtagTimeline(id);
|
this._unsubscribe();
|
||||||
this.refresh = () => refreshHashtagTimeline(id);
|
this._subscribe(this.props.dispatch, nextProps.params.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this._unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
this.props.dispatch(expandHashtagTimeline(this.props.params.id));
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { columnId, multiColumn } = this.props;
|
const { hasUnread, columnId, multiColumn } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
|
const pinned = !!columnId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Column ref={this.setRef}>
|
||||||
expand={this.expand}
|
<ColumnHeader
|
||||||
refresh={this.refresh}
|
|
||||||
streamId={`hashtag&tag=${id}`}
|
|
||||||
columnName='HASHTAG'
|
|
||||||
columnProps={{ id }}
|
|
||||||
columnId={columnId}
|
|
||||||
mulitColumn={multiColumn}
|
|
||||||
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
|
||||||
icon='hashtag'
|
icon='hashtag'
|
||||||
|
active={hasUnread}
|
||||||
title={id}
|
title={id}
|
||||||
scrollName='hashtag_timeline'
|
onPin={this.handlePin}
|
||||||
timelineId={`hashtag:${id}`}
|
onMove={this.handleMove}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
pinned={pinned}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`hashtag_timeline-${columnId}`}
|
||||||
|
timelineId={`hashtag:${id}`}
|
||||||
|
loadMore={this.handleLoadMore}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import React from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
import Column from '../../components/column';
|
||||||
|
import ColumnHeader from '../../components/column_header';
|
||||||
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import Timeline from '../timeline';
|
|
||||||
import Link from 'react-router-dom/Link';
|
import Link from 'react-router-dom/Link';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -12,6 +15,7 @@ const messages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
||||||
hasFollows: state.getIn(['accounts_counters', state.getIn(['meta', 'me']), 'following_count']) > 0,
|
hasFollows: state.getIn(['accounts_counters', state.getIn(['meta', 'me']), 'following_count']) > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,14 +24,44 @@ const mapStateToProps = state => ({
|
|||||||
export default class HomeTimeline extends React.PureComponent {
|
export default class HomeTimeline extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
hasUnread: PropTypes.bool,
|
||||||
hasFollows: PropTypes.bool,
|
hasFollows: PropTypes.bool,
|
||||||
columnId: PropTypes.string,
|
columnId: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handlePin = () => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
|
||||||
|
if (columnId) {
|
||||||
|
dispatch(removeColumn(columnId));
|
||||||
|
} else {
|
||||||
|
dispatch(addColumn('HOME', {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMove = (dir) => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
dispatch(moveColumn(columnId, dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
this.props.dispatch(expandHomeTimeline());
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasFollows, columnId, multiColumn } = this.props;
|
const { intl, hasUnread, hasFollows, columnId, multiColumn } = this.props;
|
||||||
|
const pinned = !!columnId;
|
||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
@@ -38,18 +72,28 @@ export default class HomeTimeline extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Column ref={this.setRef}>
|
||||||
expand={expandHomeTimeline}
|
<ColumnHeader
|
||||||
columnName='HOME'
|
|
||||||
columnId={columnId}
|
|
||||||
mulitColumn={multiColumn}
|
|
||||||
emptyMessage={emptyMessage}
|
|
||||||
icon='home'
|
icon='home'
|
||||||
|
active={hasUnread}
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
settings={<ColumnSettingsContainer />}
|
onPin={this.handlePin}
|
||||||
scrollName='home_timeline'
|
onMove={this.handleMove}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
pinned={pinned}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
>
|
||||||
|
<ColumnSettingsContainer />
|
||||||
|
</ColumnHeader>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`home_timeline-${columnId}`}
|
||||||
|
loadMore={this.handleLoadMore}
|
||||||
timelineId='home'
|
timelineId='home'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
/>
|
/>
|
||||||
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import ColumnSettingsContainer from './containers/column_settings_container';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import LoadMore from '../../components/load_more';
|
import LoadMore from '../../components/load_more';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||||
@@ -50,19 +51,27 @@ export default class Notifications extends React.PureComponent {
|
|||||||
trackScroll: true,
|
trackScroll: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dispatchExpandNotifications = debounce(() => {
|
||||||
|
this.props.dispatch(expandNotifications());
|
||||||
|
}, 300, { leading: true });
|
||||||
|
|
||||||
|
dispatchScrollToTop = debounce((top) => {
|
||||||
|
this.props.dispatch(scrollTopNotifications(top));
|
||||||
|
}, 100);
|
||||||
|
|
||||||
handleScroll = (e) => {
|
handleScroll = (e) => {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||||
const offset = scrollHeight - scrollTop - clientHeight;
|
const offset = scrollHeight - scrollTop - clientHeight;
|
||||||
this._oldScrollPosition = scrollHeight - scrollTop;
|
this._oldScrollPosition = scrollHeight - scrollTop;
|
||||||
|
|
||||||
if (250 > offset && !this.props.isLoading) {
|
if (250 > offset && this.props.hasMore && !this.props.isLoading) {
|
||||||
if (this.props.hasMore) {
|
this.dispatchExpandNotifications();
|
||||||
this.props.dispatch(expandNotifications());
|
|
||||||
}
|
}
|
||||||
} else if (scrollTop < 100) {
|
|
||||||
this.props.dispatch(scrollTopNotifications(true));
|
if (scrollTop < 100) {
|
||||||
|
this.dispatchScrollToTop(true);
|
||||||
} else {
|
} else {
|
||||||
this.props.dispatch(scrollTopNotifications(false));
|
this.dispatchScrollToTop(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +83,7 @@ export default class Notifications extends React.PureComponent {
|
|||||||
|
|
||||||
handleLoadMore = (e) => {
|
handleLoadMore = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.dispatch(expandNotifications());
|
this.dispatchExpandNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePin = () => {
|
handlePin = () => {
|
||||||
|
|||||||
@@ -1,44 +1,144 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
import Column from '../../components/column';
|
||||||
|
import ColumnHeader from '../../components/column_header';
|
||||||
import {
|
import {
|
||||||
refreshPublicTimeline,
|
refreshPublicTimeline,
|
||||||
expandPublicTimeline,
|
expandPublicTimeline,
|
||||||
|
updateTimeline,
|
||||||
|
deleteFromTimelines,
|
||||||
|
connectTimeline,
|
||||||
|
disconnectTimeline,
|
||||||
} from '../../actions/timelines';
|
} from '../../actions/timelines';
|
||||||
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import Timeline from '../timeline';
|
import createStream from '../../stream';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
|
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
|
accessToken: state.getIn(['meta', 'access_token']),
|
||||||
|
});
|
||||||
|
|
||||||
|
@connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
export default class PublicTimeline extends React.PureComponent {
|
export default class PublicTimeline extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
columnId: PropTypes.string,
|
columnId: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
streamingAPIBaseURL: PropTypes.string.isRequired,
|
||||||
|
accessToken: PropTypes.string.isRequired,
|
||||||
|
hasUnread: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handlePin = () => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
|
||||||
|
if (columnId) {
|
||||||
|
dispatch(removeColumn(columnId));
|
||||||
|
} else {
|
||||||
|
dispatch(addColumn('PUBLIC', {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMove = (dir) => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
dispatch(moveColumn(columnId, dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||||
|
|
||||||
|
dispatch(refreshPublicTimeline());
|
||||||
|
|
||||||
|
if (typeof this._subscription !== 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
|
||||||
|
|
||||||
|
connected () {
|
||||||
|
dispatch(connectTimeline('public'));
|
||||||
|
},
|
||||||
|
|
||||||
|
reconnected () {
|
||||||
|
dispatch(connectTimeline('public'));
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnected () {
|
||||||
|
dispatch(disconnectTimeline('public'));
|
||||||
|
},
|
||||||
|
|
||||||
|
received (data) {
|
||||||
|
switch(data.event) {
|
||||||
|
case 'update':
|
||||||
|
dispatch(updateTimeline('public', JSON.parse(data.payload)));
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (typeof this._subscription !== 'undefined') {
|
||||||
|
this._subscription.close();
|
||||||
|
this._subscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
this.props.dispatch(expandPublicTimeline());
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, columnId, multiColumn } = this.props;
|
const { intl, columnId, hasUnread, multiColumn } = this.props;
|
||||||
|
const pinned = !!columnId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Column ref={this.setRef}>
|
||||||
expand={expandPublicTimeline}
|
<ColumnHeader
|
||||||
refresh={refreshPublicTimeline}
|
|
||||||
streamId='public'
|
|
||||||
columnName='PUBLIC'
|
|
||||||
columnId={columnId}
|
|
||||||
mulitColumn={multiColumn}
|
|
||||||
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />}
|
|
||||||
icon='globe'
|
icon='globe'
|
||||||
|
active={hasUnread}
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
settings={<ColumnSettingsContainer />}
|
onPin={this.handlePin}
|
||||||
scrollName='public_timeline'
|
onMove={this.handleMove}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
pinned={pinned}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
>
|
||||||
|
<ColumnSettingsContainer />
|
||||||
|
</ColumnHeader>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
timelineId='public'
|
timelineId='public'
|
||||||
|
loadMore={this.handleLoadMore}
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`public_timeline-${columnId}`}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />}
|
||||||
/>
|
/>
|
||||||
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ export default class ActionBar extends React.PureComponent {
|
|||||||
|
|
||||||
handleReport = () => {
|
handleReport = () => {
|
||||||
this.props.onReport(this.props.status);
|
this.props.onReport(this.props.status);
|
||||||
this.context.router.history.push('/report');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||||||
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
|
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
|
||||||
|
|
||||||
let media = '';
|
let media = '';
|
||||||
|
let mediaIcon = null;
|
||||||
let applicationLink = '';
|
let applicationLink = '';
|
||||||
|
|
||||||
if (status.get('media_attachments').size > 0) {
|
if (status.get('media_attachments').size > 0) {
|
||||||
@@ -45,12 +46,12 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||||||
media = <AttachmentList media={status.get('media_attachments')} />;
|
media = <AttachmentList media={status.get('media_attachments')} />;
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />;
|
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />;
|
||||||
|
mediaIcon = 'video-camera';
|
||||||
} else {
|
} else {
|
||||||
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
|
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
|
||||||
|
mediaIcon = 'picture-o';
|
||||||
}
|
}
|
||||||
} else if (status.get('spoiler_text').length === 0) {
|
} else media = <CardContainer statusId={status.get('id')} />;
|
||||||
media = <CardContainer statusId={status.get('id')} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.get('application')) {
|
if (status.get('application')) {
|
||||||
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
|
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
|
||||||
@@ -63,9 +64,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||||||
<DisplayName account={status.get('account')} />
|
<DisplayName account={status.get('account')} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<StatusContent status={status} />
|
<StatusContent status={status} mediaIcon={mediaIcon}>{media}</StatusContent>
|
||||||
|
|
||||||
{media}
|
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
|
||||||
import Column from '../../components/column';
|
|
||||||
import ColumnHeader from '../../components/column_header';
|
|
||||||
import {
|
|
||||||
updateTimeline,
|
|
||||||
deleteFromTimelines,
|
|
||||||
connectTimeline,
|
|
||||||
disconnectTimeline,
|
|
||||||
} from '../../actions/timelines';
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
|
||||||
import createStream from '../../stream';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, ownprops) => ({
|
|
||||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
|
||||||
accessToken: state.getIn(['meta', 'access_token']),
|
|
||||||
hasUnread: state.getIn(['timelines', ownprops.timelineId, 'unread']) > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
|
||||||
export default class Timeline extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
streamingAPIBaseURL: PropTypes.string.isRequired,
|
|
||||||
accessToken: PropTypes.string.isRequired,
|
|
||||||
expand: PropTypes.func.isRequired,
|
|
||||||
refresh: PropTypes.func,
|
|
||||||
streamId: PropTypes.string,
|
|
||||||
hasUnread: PropTypes.bool,
|
|
||||||
columnName: PropTypes.string.isRequired,
|
|
||||||
columnProps: PropTypes.object,
|
|
||||||
columnId: PropTypes.string,
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
emptyMessage: PropTypes.oneOfType([
|
|
||||||
PropTypes.element,
|
|
||||||
PropTypes.string,
|
|
||||||
]),
|
|
||||||
icon: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
settings: PropTypes.element,
|
|
||||||
scrollName: PropTypes.string.isRequired,
|
|
||||||
timelineId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePin = () => {
|
|
||||||
const { columnName, columnProps, columnId, dispatch } = this.props;
|
|
||||||
|
|
||||||
if (columnId) {
|
|
||||||
dispatch(removeColumn(columnId));
|
|
||||||
} else {
|
|
||||||
dispatch(addColumn(columnName, columnProps || {}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMove = (dir) => {
|
|
||||||
const { columnId, dispatch } = this.props;
|
|
||||||
dispatch(moveColumn(columnId, dir));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleHeaderClick = () => {
|
|
||||||
this.column.scrollTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.column = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoadMore = () => {
|
|
||||||
this.props.dispatch(this.props.expand());
|
|
||||||
}
|
|
||||||
|
|
||||||
_subscribe (dispatch, streamId, timelineId) {
|
|
||||||
const { streamingAPIBaseURL, accessToken } = this.props;
|
|
||||||
|
|
||||||
if (!streamId || !timelineId) return;
|
|
||||||
|
|
||||||
this.subscription = createStream(streamingAPIBaseURL, accessToken, streamId, {
|
|
||||||
|
|
||||||
connected () {
|
|
||||||
dispatch(connectTimeline(timelineId));
|
|
||||||
},
|
|
||||||
|
|
||||||
reconnected () {
|
|
||||||
dispatch(connectTimeline(timelineId));
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnected () {
|
|
||||||
dispatch(disconnectTimeline(timelineId));
|
|
||||||
},
|
|
||||||
|
|
||||||
received (data) {
|
|
||||||
switch(data.event) {
|
|
||||||
case 'update':
|
|
||||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
|
|
||||||
break;
|
|
||||||
case 'delete':
|
|
||||||
dispatch(deleteFromTimelines(data.payload));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_unsubscribe () {
|
|
||||||
if (typeof this.subscription !== 'undefined') {
|
|
||||||
this.subscription.close();
|
|
||||||
this.subscription = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { dispatch, refresh, streamId, timelineId } = this.props;
|
|
||||||
|
|
||||||
if (typeof refresh !== 'function') return;
|
|
||||||
|
|
||||||
dispatch(refresh());
|
|
||||||
this._subscribe(dispatch, streamId, timelineId);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
if (nextProps.streamId !== this.props.streamId || nextProps.timelineId !== this.props.timelineId) {
|
|
||||||
|
|
||||||
if (typeof refresh !== 'function') return;
|
|
||||||
|
|
||||||
this.props.dispatch(this.props.refresh());
|
|
||||||
this._unsubscribe();
|
|
||||||
this._subscribe(this.props.dispatch, nextProps.streamId, nextProps.timelineId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
this._unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const {
|
|
||||||
hasUnread,
|
|
||||||
columnId,
|
|
||||||
multiColumn,
|
|
||||||
emptyMessage,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
settings,
|
|
||||||
scrollName,
|
|
||||||
timelineId,
|
|
||||||
} = this.props;
|
|
||||||
const pinned = !!columnId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column ref={this.setRef}>
|
|
||||||
<ColumnHeader
|
|
||||||
icon={icon}
|
|
||||||
active={hasUnread}
|
|
||||||
title={title}
|
|
||||||
onPin={this.handlePin}
|
|
||||||
onMove={this.handleMove}
|
|
||||||
onClick={this.handleHeaderClick}
|
|
||||||
pinned={pinned}
|
|
||||||
multiColumn={multiColumn}
|
|
||||||
>
|
|
||||||
{settings}
|
|
||||||
</ColumnHeader>
|
|
||||||
|
|
||||||
<StatusListContainer
|
|
||||||
trackScroll={!pinned}
|
|
||||||
scrollKey={`${scrollName}-${columnId}`}
|
|
||||||
loadMore={this.handleLoadMore}
|
|
||||||
timelineId={timelineId}
|
|
||||||
emptyMessage={emptyMessage}
|
|
||||||
/>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export default class ImageLoader extends React.PureComponent {
|
export default class ImageLoader extends React.PureComponent {
|
||||||
|
|
||||||
@@ -20,31 +21,113 @@ export default class ImageLoader extends React.PureComponent {
|
|||||||
error: false,
|
error: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
removers = [];
|
||||||
this._loadImage(this.props.src);
|
|
||||||
|
get canvasContext() {
|
||||||
|
if (!this.canvas) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
|
||||||
|
return this._canvasContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(props) {
|
componentDidMount () {
|
||||||
this._loadImage(props.src);
|
this.loadImage(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadImage(src) {
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (this.props.src !== nextProps.src) {
|
||||||
|
this.loadImage(nextProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadImage (props) {
|
||||||
|
this.removeEventListeners();
|
||||||
|
this.setState({ loading: true, error: false });
|
||||||
|
Promise.all([
|
||||||
|
this.loadPreviewCanvas(props),
|
||||||
|
this.loadOriginalImage(props),
|
||||||
|
])
|
||||||
|
.then(() => {
|
||||||
|
this.setState({ loading: false, error: false });
|
||||||
|
this.clearPreviewCanvas();
|
||||||
|
})
|
||||||
|
.catch(() => this.setState({ loading: false, error: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
|
const removeEventListeners = () => {
|
||||||
|
image.removeEventListener('error', handleError);
|
||||||
|
image.removeEventListener('load', handleLoad);
|
||||||
|
};
|
||||||
|
const handleError = () => {
|
||||||
|
removeEventListeners();
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
const handleLoad = () => {
|
||||||
|
removeEventListeners();
|
||||||
|
this.canvasContext.drawImage(image, 0, 0, width, height);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
image.addEventListener('error', handleError);
|
||||||
|
image.addEventListener('load', handleLoad);
|
||||||
|
image.src = previewSrc;
|
||||||
|
this.removers.push(removeEventListeners);
|
||||||
|
})
|
||||||
|
|
||||||
image.onerror = () => this.setState({ loading: false, error: true });
|
clearPreviewCanvas () {
|
||||||
image.onload = () => this.setState({ loading: false, error: false });
|
const { width, height } = this.canvas;
|
||||||
|
this.canvasContext.clearRect(0, 0, width, height);
|
||||||
image.src = src;
|
|
||||||
|
|
||||||
this.setState({ loading: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
loadOriginalImage = ({ src }) => new Promise((resolve, reject) => {
|
||||||
const { alt, src, previewSrc, width, height } = this.props;
|
const image = new Image();
|
||||||
|
const removeEventListeners = () => {
|
||||||
|
image.removeEventListener('error', handleError);
|
||||||
|
image.removeEventListener('load', handleLoad);
|
||||||
|
};
|
||||||
|
const handleError = () => {
|
||||||
|
removeEventListeners();
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
const handleLoad = () => {
|
||||||
|
removeEventListeners();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
image.addEventListener('error', handleError);
|
||||||
|
image.addEventListener('load', handleLoad);
|
||||||
|
image.src = src;
|
||||||
|
this.removers.push(removeEventListeners);
|
||||||
|
});
|
||||||
|
|
||||||
|
removeEventListeners () {
|
||||||
|
this.removers.forEach(listeners => listeners());
|
||||||
|
this.removers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanvasRef = c => {
|
||||||
|
this.canvas = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { alt, src, width, height } = this.props;
|
||||||
const { loading } = this.state;
|
const { loading } = this.state;
|
||||||
|
|
||||||
|
const className = classNames('image-loader', {
|
||||||
|
'image-loader--loading': loading,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='image-loader'>
|
<div className={className}>
|
||||||
|
<canvas
|
||||||
|
className='image-loader__preview-canvas'
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
ref={this.setCanvasRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!loading && (
|
||||||
<img
|
<img
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className='image-loader__img'
|
className='image-loader__img'
|
||||||
@@ -52,14 +135,7 @@ export default class ImageLoader extends React.PureComponent {
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{loading &&
|
|
||||||
<img
|
|
||||||
alt=''
|
|
||||||
src={previewSrc}
|
|
||||||
className='image-loader__preview-img'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import OnboardingModal from './onboarding_modal';
|
|||||||
import VideoModal from './video_modal';
|
import VideoModal from './video_modal';
|
||||||
import BoostModal from './boost_modal';
|
import BoostModal from './boost_modal';
|
||||||
import ConfirmationModal from './confirmation_modal';
|
import ConfirmationModal from './confirmation_modal';
|
||||||
|
import ReportModal from './report_modal';
|
||||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ const MODAL_COMPONENTS = {
|
|||||||
'VIDEO': VideoModal,
|
'VIDEO': VideoModal,
|
||||||
'BOOST': BoostModal,
|
'BOOST': BoostModal,
|
||||||
'CONFIRM': ConfirmationModal,
|
'CONFIRM': ConfirmationModal,
|
||||||
|
'REPORT': ReportModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends React.PureComponent {
|
export default class ModalRoot extends React.PureComponent {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ReactSwipeable from 'react-swipeable';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Permalink from '../../../components/permalink';
|
import Permalink from '../../../components/permalink';
|
||||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||||
@@ -274,7 +275,7 @@ export default class OnboardingModal extends React.PureComponent {
|
|||||||
<div className='modal-root__modal onboarding-modal'>
|
<div className='modal-root__modal onboarding-modal'>
|
||||||
<TransitionMotion styles={styles}>
|
<TransitionMotion styles={styles}>
|
||||||
{interpolatedStyles => (
|
{interpolatedStyles => (
|
||||||
<div className='onboarding-modal__pager'>
|
<ReactSwipeable onSwipedRight={this.handlePrev} onSwipedLeft={this.handleNext} className='onboarding-modal__pager'>
|
||||||
{interpolatedStyles.map(({ key, data, style }, i) => {
|
{interpolatedStyles.map(({ key, data, style }, i) => {
|
||||||
const className = classNames('onboarding-modal__page__wrapper', {
|
const className = classNames('onboarding-modal__page__wrapper', {
|
||||||
'onboarding-modal__page__wrapper--active': i === currentIndex,
|
'onboarding-modal__page__wrapper--active': i === currentIndex,
|
||||||
@@ -283,7 +284,7 @@ export default class OnboardingModal extends React.PureComponent {
|
|||||||
<div key={key} style={style} className={className}>{data}</div>
|
<div key={key} style={style} className={className}>{data}</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</ReactSwipeable>
|
||||||
)}
|
)}
|
||||||
</TransitionMotion>
|
</TransitionMotion>
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { changeReportComment, submitReport } from '../../actions/reports';
|
import { changeReportComment, submitReport } from '../../../actions/reports';
|
||||||
import { refreshAccountTimeline } from '../../actions/timelines';
|
import { refreshAccountTimeline } from '../../../actions/timelines';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Column from '../ui/components/column';
|
import { makeGetAccount } from '../../../selectors';
|
||||||
import Button from '../../components/button';
|
|
||||||
import { makeGetAccount } from '../../selectors';
|
|
||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||||
import StatusCheckBox from './containers/status_check_box_container';
|
import StatusCheckBox from '../../report/containers/status_check_box_container';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import Button from '../../../components/button';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'report.heading', defaultMessage: 'New report' },
|
|
||||||
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
|
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
|
||||||
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
||||||
});
|
});
|
||||||
@@ -37,11 +35,7 @@ const makeMapStateToProps = () => {
|
|||||||
|
|
||||||
@connect(makeMapStateToProps)
|
@connect(makeMapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
export default class Report extends React.PureComponent {
|
export default class ReportModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isSubmitting: PropTypes.bool,
|
isSubmitting: PropTypes.bool,
|
||||||
@@ -52,17 +46,15 @@ export default class Report extends React.PureComponent {
|
|||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
handleCommentChange = (e) => {
|
||||||
if (!this.props.account) {
|
this.props.dispatch(changeReportComment(e.target.value));
|
||||||
this.context.router.history.replace('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
this.props.dispatch(submitReport());
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
if (!this.props.account) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.dispatch(refreshAccountTimeline(this.props.account.get('id')));
|
this.props.dispatch(refreshAccountTimeline(this.props.account.get('id')));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,15 +64,6 @@ export default class Report extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommentChange = (e) => {
|
|
||||||
this.props.dispatch(changeReportComment(e.target.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit = () => {
|
|
||||||
this.props.dispatch(submitReport());
|
|
||||||
this.context.router.history.replace('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, comment, intl, statusIds, isSubmitting } = this.props;
|
const { account, comment, intl, statusIds, isSubmitting } = this.props;
|
||||||
|
|
||||||
@@ -89,36 +72,33 @@ export default class Report extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column heading={intl.formatMessage(messages.heading)} icon='flag'>
|
<div className='modal-root__modal report-modal'>
|
||||||
<ColumnBackButtonSlim />
|
<div className='report-modal__target'>
|
||||||
|
<FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} />
|
||||||
<div className='report scrollable'>
|
|
||||||
<div className='report__target'>
|
|
||||||
<FormattedMessage id='report.target' defaultMessage='Reporting' />
|
|
||||||
<strong>{account.get('acct')}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='scrollable report__statuses'>
|
<div className='report-modal__container'>
|
||||||
|
<div className='report-modal__statuses'>
|
||||||
<div>
|
<div>
|
||||||
{statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
|
{statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='report__textarea-wrapper'>
|
<div className='report-modal__comment'>
|
||||||
<textarea
|
<textarea
|
||||||
className='report__textarea'
|
className='setting-text light'
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={this.handleCommentChange}
|
onChange={this.handleCommentChange}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='report__submit'>
|
<div className='report-modal__action-bar'>
|
||||||
<div className='report__submit-button'><Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /></div>
|
<Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Column>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ import { refreshHomeTimeline } from '../../actions/timelines';
|
|||||||
import { refreshNotifications } from '../../actions/notifications';
|
import { refreshNotifications } from '../../actions/notifications';
|
||||||
import UploadArea from './components/upload_area';
|
import UploadArea from './components/upload_area';
|
||||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||||
|
|
||||||
import Status from '../../features/status';
|
import Status from '../../features/status';
|
||||||
import GettingStarted from '../../features/getting_started';
|
import GettingStarted from '../../features/getting_started';
|
||||||
import PublicTimeline from '../../features/public_timeline';
|
import PublicTimeline from '../../features/public_timeline';
|
||||||
@@ -35,7 +34,6 @@ import GenericNotFound from '../../features/generic_not_found';
|
|||||||
import FavouritedStatuses from '../../features/favourited_statuses';
|
import FavouritedStatuses from '../../features/favourited_statuses';
|
||||||
import Blocks from '../../features/blocks';
|
import Blocks from '../../features/blocks';
|
||||||
import Mutes from '../../features/mutes';
|
import Mutes from '../../features/mutes';
|
||||||
import Report from '../../features/report';
|
|
||||||
|
|
||||||
// Small wrapper to pass multiColumn to the route components
|
// Small wrapper to pass multiColumn to the route components
|
||||||
const WrappedSwitch = ({ multiColumn, children }) => (
|
const WrappedSwitch = ({ multiColumn, children }) => (
|
||||||
@@ -206,7 +204,6 @@ export default class UI extends React.PureComponent {
|
|||||||
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
|
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
|
||||||
<WrappedRoute path='/blocks' component={Blocks} content={children} />
|
<WrappedRoute path='/blocks' component={Blocks} content={children} />
|
||||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||||
<WrappedRoute path='/report' component={Report} content={children} />
|
|
||||||
|
|
||||||
<WrappedRoute component={GenericNotFound} content={children} />
|
<WrappedRoute component={GenericNotFound} content={children} />
|
||||||
</WrappedSwitch>
|
</WrappedSwitch>
|
||||||
|
|||||||
@@ -191,14 +191,6 @@
|
|||||||
{
|
{
|
||||||
"defaultMessage": "{name} boosted",
|
"defaultMessage": "{name} boosted",
|
||||||
"id": "status.reblogged_by"
|
"id": "status.reblogged_by"
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Collapse",
|
|
||||||
"id": "status.collapse"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Uncollapse",
|
|
||||||
"id": "status.uncollapse"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/components/status.json"
|
"path": "app/javascript/mastodon/components/status.json"
|
||||||
@@ -1135,6 +1127,23 @@
|
|||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/ui/components/onboarding_modal.json"
|
"path": "app/javascript/mastodon/features/ui/components/onboarding_modal.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Additional comments",
|
||||||
|
"id": "report.placeholder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Submit",
|
||||||
|
"id": "report.submit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Report {target}",
|
||||||
|
"id": "report.target"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/ui/components/report_modal.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -136,14 +136,13 @@
|
|||||||
"privacy.unlisted.long": "Do not post to public timelines",
|
"privacy.unlisted.long": "Do not post to public timelines",
|
||||||
"privacy.unlisted.short": "Unlisted",
|
"privacy.unlisted.short": "Unlisted",
|
||||||
"reply_indicator.cancel": "Cancel",
|
"reply_indicator.cancel": "Cancel",
|
||||||
"report.heading": "New report",
|
"report.heading": "Report {target}",
|
||||||
"report.placeholder": "Additional comments",
|
"report.placeholder": "Additional comments",
|
||||||
"report.submit": "Submit",
|
"report.submit": "Submit",
|
||||||
"report.target": "Reporting",
|
"report.target": "Reporting {target}",
|
||||||
"search.placeholder": "Search",
|
"search.placeholder": "Search",
|
||||||
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
|
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
|
||||||
"status.cannot_reblog": "This post cannot be boosted",
|
"status.cannot_reblog": "This post cannot be boosted",
|
||||||
"status.collapse": "Collapse",
|
|
||||||
"status.delete": "Delete",
|
"status.delete": "Delete",
|
||||||
"status.favourite": "Favourite",
|
"status.favourite": "Favourite",
|
||||||
"status.load_more": "Load more",
|
"status.load_more": "Load more",
|
||||||
@@ -160,7 +159,6 @@
|
|||||||
"status.sensitive_warning": "Sensitive content",
|
"status.sensitive_warning": "Sensitive content",
|
||||||
"status.show_less": "Show less",
|
"status.show_less": "Show less",
|
||||||
"status.show_more": "Show more",
|
"status.show_more": "Show more",
|
||||||
"status.uncollapse": "Uncollapse",
|
|
||||||
"status.unmute_conversation": "Unmute conversation",
|
"status.unmute_conversation": "Unmute conversation",
|
||||||
"tabs_bar.compose": "Compose",
|
"tabs_bar.compose": "Compose",
|
||||||
"tabs_bar.federated_timeline": "Federated",
|
"tabs_bar.federated_timeline": "Federated",
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
"column.notifications": "Notifications",
|
"column.notifications": "Notifications",
|
||||||
"column.public": "Fil public global",
|
"column.public": "Fil public global",
|
||||||
"column_back_button.label": "Retour",
|
"column_back_button.label": "Retour",
|
||||||
"column_header.pin": "Pin",
|
"column_header.pin": "Épingler",
|
||||||
"column_header.unpin": "Unpin",
|
"column_header.unpin": "Retirer",
|
||||||
"column_subheading.navigation": "Navigation",
|
"column_subheading.navigation": "Navigation",
|
||||||
"column_subheading.settings": "Paramètres",
|
"column_subheading.settings": "Paramètres",
|
||||||
"compose_form.lock_disclaimer": "Votre compte n'est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
|
"compose_form.lock_disclaimer": "Votre compte n'est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
"notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
|
"notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
|
||||||
"notifications.column_settings.alert": "Notifications locales",
|
"notifications.column_settings.alert": "Notifications locales",
|
||||||
"notifications.column_settings.favourite": "Favoris :",
|
"notifications.column_settings.favourite": "Favoris :",
|
||||||
"notifications.column_settings.follow": "Nouveaux abonné⋅e⋅s :",
|
"notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :",
|
||||||
"notifications.column_settings.mention": "Mentions :",
|
"notifications.column_settings.mention": "Mentions :",
|
||||||
"notifications.column_settings.reblog": "Partages :",
|
"notifications.column_settings.reblog": "Partages :",
|
||||||
"notifications.column_settings.show": "Afficher dans la colonne",
|
"notifications.column_settings.show": "Afficher dans la colonne",
|
||||||
|
|||||||
@@ -136,10 +136,10 @@
|
|||||||
"privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
|
"privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
|
||||||
"privacy.unlisted.short": "Niewidoczne",
|
"privacy.unlisted.short": "Niewidoczne",
|
||||||
"reply_indicator.cancel": "Anuluj",
|
"reply_indicator.cancel": "Anuluj",
|
||||||
"report.heading": "Nowe zgłoszenie",
|
"report.heading": "Zgłoś {target}",
|
||||||
"report.placeholder": "Dodatkowe komentarze",
|
"report.placeholder": "Dodatkowe komentarze",
|
||||||
"report.submit": "Wyślij",
|
"report.submit": "Wyślij",
|
||||||
"report.target": "Zgłaszanie",
|
"report.target": "Zgłaszanie {target}",
|
||||||
"search.placeholder": "Szukaj",
|
"search.placeholder": "Szukaj",
|
||||||
"search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
|
"search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
|
||||||
"status.cannot_reblog": "Ten post nie może zostać podbity",
|
"status.cannot_reblog": "Ten post nie może zostać podbity",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
require('../styles/custom.scss');
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
@mixin avatar-radius() {
|
@mixin avatar-radius() {
|
||||||
border-radius: $ui-avatar-border-size;
|
border-radius: 4px;
|
||||||
background: transparent no-repeat;
|
background: transparent no-repeat;
|
||||||
background-position: 50%;
|
background-position: 50%;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
|
|||||||
@@ -172,14 +172,16 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@include avatar-size(80px);
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@include avatar-radius();
|
|
||||||
@include avatar-size(80px);
|
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 48px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,16 +46,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@include avatar-size(120px);
|
width: 120px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@include avatar-radius();
|
width: 120px;
|
||||||
@include avatar-size(120px);
|
height: 120px;
|
||||||
display: block;
|
display: block;
|
||||||
|
border-radius: 120px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,14 +283,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@include avatar-size(60px);
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
float: left;
|
float: left;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@include avatar-radius();
|
|
||||||
@include avatar-size(60px);
|
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,14 +359,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
@include avatar-size(48px);
|
|
||||||
float: left;
|
float: left;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@include avatar-radius();
|
|
||||||
display: block;
|
display: block;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-name {
|
.display-name {
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
color: $ui-primary-color;
|
color: $ui-primary-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.positive-hint {
|
||||||
|
color: $valid-value-color;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple_form {
|
.simple_form {
|
||||||
|
|||||||
@@ -58,37 +58,6 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-collapsable {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.column-collapsable__content {
|
|
||||||
overflow: auto;
|
|
||||||
transition: 300ms ease;
|
|
||||||
opacity: 1;
|
|
||||||
max-height: 70vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed .column-collapsable__content {
|
|
||||||
height: 0 !important;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-collapsable__button {
|
|
||||||
color: $primary-text-color;
|
|
||||||
background: lighten($ui-base-color, 8%);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $primary-text-color;
|
|
||||||
background: lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed .column-collapsable__button {
|
|
||||||
color: $ui-primary-color;
|
|
||||||
background: lighten($ui-base-color, 4%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-icon {
|
.column-icon {
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
color: $ui-primary-color;
|
color: $ui-primary-color;
|
||||||
@@ -456,11 +425,9 @@
|
|||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: $primary-text-color;
|
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|
||||||
.emojione {
|
.emojione {
|
||||||
@@ -503,19 +470,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__content__spoiler-link {
|
.status__content__spoiler {
|
||||||
background: lighten($ui-base-color, 30%);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: lighten($ui-base-color, 33%);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__content__text {
|
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
&.status__content__text--visible {
|
&.status__content__spoiler--visible {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,7 +482,7 @@
|
|||||||
.status__content__spoiler-link {
|
.status__content__spoiler-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: transparent;
|
background: lighten($ui-base-color, 30%);
|
||||||
border: 0;
|
border: 0;
|
||||||
color: lighten($ui-base-color, 8%);
|
color: lighten($ui-base-color, 8%);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -533,6 +491,21 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
vertical-align: bottom;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: lighten($ui-base-color, 33%);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__content__spoiler-icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0 0 5px;
|
||||||
|
border-left: 1px solid currentColor;
|
||||||
|
padding: 0 0 0 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: -2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__prepend-icon-wrapper {
|
.status__prepend-icon-wrapper {
|
||||||
@@ -544,7 +517,6 @@
|
|||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
padding-left: 68px;
|
padding-left: 68px;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: auto;
|
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@@ -601,14 +573,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.status-collapsed {
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
.status__content {
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-favourite {
|
.notification-favourite {
|
||||||
@@ -622,8 +586,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status__relative-time {
|
.status__relative-time {
|
||||||
margin-left: auto;
|
|
||||||
color: lighten($ui-base-color, 26%);
|
color: lighten($ui-base-color, 26%);
|
||||||
|
float: right;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,23 +602,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status__info {
|
.status__info {
|
||||||
margin: 2px 0 0;
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__collapse-button {
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-check-box {
|
.status-check-box {
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid $ui-secondary-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.status__content {
|
.status__content {
|
||||||
background: lighten($ui-base-color, 4%);
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,12 +797,9 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__header__wrapper {
|
.account__header {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
}
|
|
||||||
|
|
||||||
.account__header {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@@ -907,42 +864,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__metadata {
|
|
||||||
display: block;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 36px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.account__metadata-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
|
|
||||||
& > span, & > strong {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 120px;
|
|
||||||
color: $ui-primary-color;
|
|
||||||
background: lighten($ui-base-color, 13%);
|
|
||||||
font-variant: small-caps;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > strong {
|
|
||||||
flex: auto;
|
|
||||||
color: $primary-text-color;
|
|
||||||
background: $ui-base-color;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.account__action-bar {
|
.account__action-bar {
|
||||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
@@ -1004,11 +925,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.account__header__avatar {
|
.account__header__avatar {
|
||||||
@include avatar-radius();
|
background-size: 90px 90px;
|
||||||
@include avatar-size(90px);
|
|
||||||
display: block;
|
display: block;
|
||||||
|
height: 90px;
|
||||||
margin: 0 auto 10px;
|
margin: 0 auto 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-authorize {
|
.account-authorize {
|
||||||
@@ -1185,20 +1107,22 @@
|
|||||||
|
|
||||||
.image-loader {
|
.image-loader {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
|
||||||
|
|
||||||
.image-loader__preview-img {
|
&.image-loader--loading {
|
||||||
|
.image-loader__preview-canvas {
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-loader__img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
filter: blur(2px);
|
background-image: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-modal img.image-loader__preview-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-bar {
|
.navigation-bar {
|
||||||
@@ -1914,6 +1838,17 @@
|
|||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
color: $ui-base-color;
|
||||||
|
border-bottom: 2px solid lighten($ui-base-color, 27%);
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
color: $ui-base-color;
|
||||||
|
border-bottom-color: $ui-highlight-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import 'boost';
|
@import 'boost';
|
||||||
@@ -2165,11 +2100,6 @@ button.icon-button.active i.fa-retweet {
|
|||||||
transition: max-height 150ms ease-in-out, opacity 300ms linear;
|
transition: max-height 150ms ease-in-out, opacity 300ms linear;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
& > div {
|
|
||||||
background: lighten($ui-base-color, 8%);
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -2180,6 +2110,11 @@ button.icon-button.active i.fa-retweet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-header__collapsible-inner {
|
||||||
|
background: lighten($ui-base-color, 8%);
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.column-header__setting-btn {
|
.column-header__setting-btn {
|
||||||
&:hover {
|
&:hover {
|
||||||
color: lighten($ui-primary-color, 4%);
|
color: lighten($ui-primary-color, 4%);
|
||||||
@@ -2371,67 +2306,6 @@ button.icon-button.active i.fa-retweet {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report.scrollable {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report__target {
|
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
|
||||||
color: $ui-secondary-color;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
display: block;
|
|
||||||
color: $primary-text-color;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.report__statuses {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report__textarea-wrapper {
|
|
||||||
flex: 0 0 100px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report__textarea {
|
|
||||||
background: transparent;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 2px solid $ui-primary-color;
|
|
||||||
border-radius: 2px 2px 0 0;
|
|
||||||
color: $primary-text-color;
|
|
||||||
display: block;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
outline: 0;
|
|
||||||
padding: 7px 4px;
|
|
||||||
resize: vertical;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
border-bottom-color: $ui-highlight-color;
|
|
||||||
background: rgba($base-overlay-background, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.report__submit {
|
|
||||||
margin-top: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report__submit-button {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-column-indicator {
|
.empty-column-indicator {
|
||||||
color: lighten($ui-base-color, 20%);
|
color: lighten($ui-base-color, 20%);
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
@@ -3019,6 +2893,7 @@ button.icon-button.active i.fa-retweet {
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
img,
|
img,
|
||||||
|
canvas,
|
||||||
video {
|
video {
|
||||||
max-width: 80vw;
|
max-width: 80vw;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
@@ -3026,7 +2901,8 @@ button.icon-button.active i.fa-retweet {
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img,
|
||||||
|
canvas {
|
||||||
display: block;
|
display: block;
|
||||||
background: url('../images/void.png') repeat;
|
background: url('../images/void.png') repeat;
|
||||||
}
|
}
|
||||||
@@ -3212,6 +3088,7 @@ button.icon-button.active i.fa-retweet {
|
|||||||
@media screen and (max-width: 400px) {
|
@media screen and (max-width: 400px) {
|
||||||
.onboarding-modal__page-one {
|
.onboarding-modal__page-one {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-modal__page-one__elephant-friend {
|
.onboarding-modal__page-one__elephant-friend {
|
||||||
@@ -3326,7 +3203,8 @@ button.icon-button.active i.fa-retweet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.boost-modal,
|
.boost-modal,
|
||||||
.confirmation-modal {
|
.confirmation-modal,
|
||||||
|
.report-modal {
|
||||||
background: lighten($ui-secondary-color, 8%);
|
background: lighten($ui-secondary-color, 8%);
|
||||||
color: $ui-base-color;
|
color: $ui-base-color;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -3362,7 +3240,8 @@ button.icon-button.active i.fa-retweet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.boost-modal__action-bar,
|
.boost-modal__action-bar,
|
||||||
.confirmation-modal__action-bar {
|
.confirmation-modal__action-bar,
|
||||||
|
.report-modal__action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: $ui-secondary-color;
|
background: $ui-secondary-color;
|
||||||
@@ -3398,6 +3277,23 @@ button.icon-button.active i.fa-retweet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-modal__statuses,
|
||||||
|
.report-modal__comment {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-modal__statuses {
|
||||||
|
min-height: 20vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-modal__comment {
|
||||||
|
.setting-text {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.confirmation-modal__action-bar {
|
.confirmation-modal__action-bar {
|
||||||
.confirmation-modal__cancel-button {
|
.confirmation-modal__cancel-button {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -3413,7 +3309,8 @@ button.icon-button.active i.fa-retweet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirmation-modal__container {
|
.confirmation-modal__container,
|
||||||
|
.report-modal__target {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -3534,10 +3431,15 @@ button.icon-button.active i.fa-retweet {
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
display: block;
|
display: flex;
|
||||||
height: 100%;
|
align-items: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&,
|
||||||
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-gallery__gifv {
|
.media-gallery__gifv {
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
@import 'application';
|
|
||||||
|
|
||||||
@media screen and (min-width: 1300px) {
|
|
||||||
.column {
|
|
||||||
flex-grow: 1 !important;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer {
|
|
||||||
flex-grow: 1 !important;
|
|
||||||
flex-basis: 200px !important;
|
|
||||||
min-width: 268px;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted {
|
|
||||||
.status__content p, .status__content a {
|
|
||||||
color: lighten($ui-base-color, 35%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__display-name strong {
|
|
||||||
color: lighten($ui-base-color, 35%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status time:after,
|
|
||||||
.detailed-status__datetime span:after {
|
|
||||||
font: normal normal normal 14px/1 FontAwesome;
|
|
||||||
content: "\00a0\00a0\f08e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-form__buttons button.active:last-child {
|
|
||||||
color:$ui-secondary-color;
|
|
||||||
background-color: $ui-highlight-color;
|
|
||||||
border-radius:3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-body .mascot {
|
|
||||||
display:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screenshot-with-signup {
|
|
||||||
min-height:300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screenshot-with-signup .closed-registrations-message,
|
|
||||||
.screenshot-with-signup form {
|
|
||||||
background-color: rgba(0,0,0,0.7);
|
|
||||||
margin:auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screenshot-with-signup .closed-registrations-message .clock {
|
|
||||||
font-size:150%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer .drawer__inner {
|
|
||||||
overflow: visible;
|
|
||||||
height:inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer__pager {
|
|
||||||
overflow-y:auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
// trying to fix @mdhughes safari problem
|
|
||||||
max-height:100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.media-gallery {
|
|
||||||
height:auto !important;
|
|
||||||
max-height:30vh;
|
|
||||||
position:relative;
|
|
||||||
margin-top:20px;
|
|
||||||
margin-left:-68px;
|
|
||||||
width: calc(100% + 80px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-gallery:before{
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
padding-top: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-gallery__item,
|
|
||||||
.media-gallery .media-spoiler{
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0 !important;
|
|
||||||
position:absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-spoiler-video:before {
|
|
||||||
content:"";
|
|
||||||
display:block;
|
|
||||||
padding-top:100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-spoiler-video,
|
|
||||||
.status__video-player,
|
|
||||||
.detailed-status > .media-spoiler,
|
|
||||||
.status > .media-spoiler {
|
|
||||||
height:auto !important;
|
|
||||||
max-height:30vh;
|
|
||||||
position:relative;
|
|
||||||
margin-top:20px;
|
|
||||||
margin-left:-68px;
|
|
||||||
width: calc(100% + 80px) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__video-player-video {
|
|
||||||
transform:unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailed-status > .media-spoiler,
|
|
||||||
.status > .media-spoiler {
|
|
||||||
height:30vh !important;
|
|
||||||
vertical-align:middle;
|
|
||||||
}
|
|
||||||
@@ -358,7 +358,6 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user_filtered_languages {
|
.user_filtered_languages {
|
||||||
|
|
||||||
& > label {
|
& > label {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
.recovery-codes {
|
.recovery-codes {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
font-size: 125%;
|
font-size: 125%;
|
||||||
|
|||||||
@@ -64,17 +64,19 @@
|
|||||||
|
|
||||||
.status__avatar {
|
.status__avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@include avatar-size(48px);
|
|
||||||
left: 14px;
|
left: 14px;
|
||||||
top: 14px;
|
top: 14px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
@include avatar-size(48px);
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@include avatar-radius();
|
|
||||||
display: block;
|
display: block;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,11 +164,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@include avatar-size(48px);
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@include avatar-radius();
|
|
||||||
display: block;
|
display: block;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,18 @@
|
|||||||
strong {
|
strong {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.inline-table {
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > tbody > tr:nth-child(odd) > td,
|
||||||
|
& > tbody > tr:nth-child(odd) > th {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
samp {
|
samp {
|
||||||
|
|||||||
@@ -26,6 +26,3 @@ $ui-base-color: $classic-base-color !default; // Darkest
|
|||||||
$ui-primary-color: $classic-primary-color !default; // Lighter
|
$ui-primary-color: $classic-primary-color !default; // Lighter
|
||||||
$ui-secondary-color: $classic-secondary-color !default; // Lightest
|
$ui-secondary-color: $classic-secondary-color !default; // Lightest
|
||||||
$ui-highlight-color: $classic-highlight-color !default; // Vibrant
|
$ui-highlight-color: $classic-highlight-color !default; // Vibrant
|
||||||
|
|
||||||
// Avatar border size (8% default, 100% for rounded avatars)
|
|
||||||
$ui-avatar-border-size: 8%;
|
|
||||||
|
|||||||
13
app/mailers/admin_mailer.rb
Normal file
13
app/mailers/admin_mailer.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AdminMailer < ApplicationMailer
|
||||||
|
def new_report(recipient, report)
|
||||||
|
@report = report
|
||||||
|
@me = recipient
|
||||||
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
|
locale_for_account(@me) do
|
||||||
|
mail to: @me.user_email, subject: I18n.t('admin_mailer.new_report.subject', instance: @instance, id: @report.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,4 +4,12 @@ class ApplicationMailer < ActionMailer::Base
|
|||||||
default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' }
|
default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' }
|
||||||
layout 'mailer'
|
layout 'mailer'
|
||||||
helper :instance
|
helper :instance
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def locale_for_account(account)
|
||||||
|
I18n.with_locale(account.user_locale || I18n.default_locale) do
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -67,12 +67,4 @@ class NotificationMailer < ApplicationMailer
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def locale_for_account(account)
|
|
||||||
I18n.with_locale(account.user_locale || I18n.default_locale) do
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,31 +8,73 @@
|
|||||||
# session_id :string not null
|
# session_id :string not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# user_agent :string default(""), not null
|
||||||
|
# ip :inet
|
||||||
|
# access_token_id :integer
|
||||||
#
|
#
|
||||||
|
|
||||||
class SessionActivation < ApplicationRecord
|
class SessionActivation < ApplicationRecord
|
||||||
LIMIT = Rails.configuration.x.max_session_activations
|
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
|
||||||
|
|
||||||
def self.active?(id)
|
delegate :token,
|
||||||
|
to: :access_token,
|
||||||
|
allow_nil: true
|
||||||
|
|
||||||
|
def detection
|
||||||
|
@detection ||= Browser.new(user_agent)
|
||||||
|
end
|
||||||
|
|
||||||
|
def browser
|
||||||
|
detection.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def platform
|
||||||
|
detection.platform.id
|
||||||
|
end
|
||||||
|
|
||||||
|
before_create :assign_access_token
|
||||||
|
before_save :assign_user_agent
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def active?(id)
|
||||||
id && where(session_id: id).exists?
|
id && where(session_id: id).exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.activate(id)
|
def activate(options = {})
|
||||||
activation = create!(session_id: id)
|
activation = create!(options)
|
||||||
purge_old
|
purge_old
|
||||||
activation
|
activation
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.deactivate(id)
|
def deactivate(id)
|
||||||
return unless id
|
return unless id
|
||||||
where(session_id: id).destroy_all
|
where(session_id: id).destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.purge_old
|
def purge_old
|
||||||
order('created_at desc').offset(LIMIT).destroy_all
|
order('created_at desc').offset(Rails.configuration.x.max_session_activations).destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.exclusive(id)
|
def exclusive(id)
|
||||||
where('session_id != ?', id).destroy_all
|
where('session_id != ?', id).destroy_all
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def assign_user_agent
|
||||||
|
self.user_agent = '' if user_agent.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_access_token
|
||||||
|
superapp = Doorkeeper::Application.find_by(superapp: true)
|
||||||
|
|
||||||
|
return if superapp.nil?
|
||||||
|
|
||||||
|
self.access_token = Doorkeeper::AccessToken.create!(application_id: superapp.id,
|
||||||
|
resource_owner_id: user_id,
|
||||||
|
scopes: 'read write follow',
|
||||||
|
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
||||||
|
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -91,8 +91,10 @@ class User < ApplicationRecord
|
|||||||
settings.auto_play_gif
|
settings.auto_play_gif
|
||||||
end
|
end
|
||||||
|
|
||||||
def activate_session
|
def activate_session(request)
|
||||||
session_activations.activate(SecureRandom.hex).session_id
|
session_activations.activate(session_id: SecureRandom.hex,
|
||||||
|
user_agent: request.user_agent,
|
||||||
|
ip: request.ip).session_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def exclusive_session(id)
|
def exclusive_session(id)
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ class SendInteractionService < BaseService
|
|||||||
return if block_notification?
|
return if block_notification?
|
||||||
|
|
||||||
envelope = salmon.pack(@xml, @source_account.keypair)
|
envelope = salmon.pack(@xml, @source_account.keypair)
|
||||||
salmon.post(@target_account.salmon_url, envelope)
|
delivery = salmon.post(@target_account.salmon_url, envelope)
|
||||||
|
raise "Delivery failed for #{target_account.salmon_url}: HTTP #{delivery.code}" unless delivery.code > 199 && delivery.code < 300
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class StatusLengthValidator < ActiveModel::Validator
|
class StatusLengthValidator < ActiveModel::Validator
|
||||||
MAX_CHARS = 512
|
MAX_CHARS = 500
|
||||||
|
|
||||||
def validate(status)
|
def validate(status)
|
||||||
return unless status.local? && !status.reblog?
|
return unless status.local? && !status.reblog?
|
||||||
|
|||||||
@@ -9,4 +9,4 @@
|
|||||||
%li= link_to t('about.get_started'), new_user_registration_path
|
%li= link_to t('about.get_started'), new_user_registration_path
|
||||||
%li= link_to t('auth.login'), new_user_session_path
|
%li= link_to t('auth.login'), new_user_session_path
|
||||||
%li= link_to t('about.terms'), terms_path
|
%li= link_to t('about.terms'), terms_path
|
||||||
%li= link_to t('about.source_code'), 'https://github.com/chronister/mastodon'
|
%li= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
.info
|
.info
|
||||||
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
|
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
|
||||||
·
|
·
|
||||||
= link_to t('about.other_instances'), 'https://instances.mastodon.xyz/'
|
= link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
|
||||||
·
|
·
|
||||||
= link_to t('about.about_this'), about_more_path
|
= link_to t('about.about_this'), about_more_path
|
||||||
|
|
||||||
@@ -82,6 +82,6 @@
|
|||||||
·
|
·
|
||||||
= link_to t('about.apps'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md'
|
= link_to t('about.apps'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md'
|
||||||
·
|
·
|
||||||
= link_to t('about.source_code'), 'https://github.com/chronister/mastodon'
|
= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
|
||||||
·
|
·
|
||||||
= link_to t('about.other_instances'), 'https://instances.mastodon.xyz/'
|
= link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
|
||||||
|
|||||||
5
app/views/admin_mailer/new_report.text.erb
Normal file
5
app/views/admin_mailer/new_report.text.erb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<%= display_name(@me) %>,
|
||||||
|
|
||||||
|
<%= raw t('admin_mailer.new_report.body', target: @report.target_account.acct, reporter: @report.account.acct) %>
|
||||||
|
|
||||||
|
<%= raw t('application_mailer.view')%> <%= admin_report_url(@report) %>
|
||||||
23
app/views/auth/registrations/_sessions.html.haml
Normal file
23
app/views/auth/registrations/_sessions.html.haml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
%h6= t 'sessions.title'
|
||||||
|
%p.muted-hint= t 'sessions.explanation'
|
||||||
|
|
||||||
|
%table.table.inline-table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th= t 'sessions.browser'
|
||||||
|
%th= t 'sessions.ip'
|
||||||
|
%th= t 'sessions.activity'
|
||||||
|
%tbody
|
||||||
|
- @sessions.each do |session|
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
%span{ title: session.user_agent }= fa_icon session_device_icon(session)
|
||||||
|
= ' '
|
||||||
|
= t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}")
|
||||||
|
%td
|
||||||
|
%samp= session.ip
|
||||||
|
%td
|
||||||
|
- if request.session['auth_id'] == session.session_id
|
||||||
|
= t 'sessions.current_session'
|
||||||
|
- else
|
||||||
|
%time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at)
|
||||||
@@ -12,6 +12,10 @@
|
|||||||
.actions
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
|
||||||
|
%hr/
|
||||||
|
|
||||||
|
= render 'sessions'
|
||||||
|
|
||||||
- if open_deletion?
|
- if open_deletion?
|
||||||
%hr/
|
%hr/
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('settings.two_factor_authentication')
|
= t('settings.two_factor_authentication')
|
||||||
|
|
||||||
%p.hint= t('two_factor_authentication.recovery_instructions')
|
%p.hint= t('two_factor_authentication.recovery_instructions_html')
|
||||||
|
|
||||||
%ol.recovery-codes
|
%ol.recovery-codes
|
||||||
- @recovery_codes.each do |code|
|
- @recovery_codes.each do |code|
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('settings.two_factor_authentication')
|
= t('settings.two_factor_authentication')
|
||||||
|
|
||||||
.simple_form
|
- if current_user.otp_required_for_login
|
||||||
%p.hint
|
%p.positive-hint
|
||||||
= t('two_factor_authentication.description_html')
|
= fa_icon 'check'
|
||||||
|
= ' '
|
||||||
|
= t 'two_factor_authentication.enabled'
|
||||||
|
|
||||||
|
%hr/
|
||||||
|
|
||||||
|
= simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f|
|
||||||
|
= f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt')
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('two_factor_authentication.disable'), type: :submit
|
||||||
|
|
||||||
|
%hr/
|
||||||
|
|
||||||
|
%h6= t('two_factor_authentication.recovery_codes')
|
||||||
|
%p.muted-hint
|
||||||
|
= t('two_factor_authentication.lost_recovery_codes')
|
||||||
|
= link_to t('two_factor_authentication.generate_recovery_codes'),
|
||||||
|
settings_two_factor_authentication_recovery_codes_path,
|
||||||
|
data: { method: :post }
|
||||||
|
|
||||||
|
- else
|
||||||
|
.simple_form
|
||||||
|
%p.hint= t('two_factor_authentication.description_html')
|
||||||
|
|
||||||
- if current_user.otp_required_for_login
|
|
||||||
= link_to t('two_factor_authentication.disable'),
|
|
||||||
settings_two_factor_authentication_path,
|
|
||||||
data: { method: :delete },
|
|
||||||
class: 'block-button'
|
|
||||||
- else
|
|
||||||
= link_to t('two_factor_authentication.setup'),
|
= link_to t('two_factor_authentication.setup'),
|
||||||
settings_two_factor_authentication_path,
|
settings_two_factor_authentication_path,
|
||||||
data: { method: :post },
|
data: { method: :post },
|
||||||
class: 'block-button'
|
class: 'block-button'
|
||||||
|
|
||||||
- if current_user.otp_required_for_login
|
|
||||||
.simple_form
|
|
||||||
%p.hint
|
|
||||||
= t('two_factor_authentication.lost_recovery_codes')
|
|
||||||
= link_to t('two_factor_authentication.generate_recovery_codes'),
|
|
||||||
settings_two_factor_authentication_recovery_codes_path,
|
|
||||||
data: { method: :post },
|
|
||||||
class: 'block-button'
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.media-spoiler
|
.media-spoiler><
|
||||||
%span= t('stream_entries.sensitive_content')
|
%span= t('stream_entries.sensitive_content')
|
||||||
%span= t('stream_entries.click_to_show')
|
%span= t('stream_entries.click_to_show')
|
||||||
|
|||||||
@@ -12,19 +12,20 @@
|
|||||||
%p{ style: 'margin-bottom: 0' }<
|
%p{ style: 'margin-bottom: 0' }<
|
||||||
%span.p-summary> #{status.spoiler_text}
|
%span.p-summary> #{status.spoiler_text}
|
||||||
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
|
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
|
||||||
.e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
|
.e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
|
||||||
|
= Formatter.instance.format(status)
|
||||||
|
|
||||||
- unless status.media_attachments.empty?
|
- unless status.media_attachments.empty?
|
||||||
- if status.media_attachments.first.video?
|
- if status.media_attachments.first.video?
|
||||||
.video-player
|
.video-player><
|
||||||
- if status.sensitive?
|
- if status.sensitive?
|
||||||
= render partial: 'stream_entries/content_spoiler'
|
= render partial: 'stream_entries/content_spoiler'
|
||||||
%video.u-video{ src: status.media_attachments.first.file.url(:original), loop: true }
|
%video.u-video{ src: status.media_attachments.first.file.url(:original), loop: true }
|
||||||
- else
|
- else
|
||||||
.detailed-status__attachments
|
.detailed-status__attachments><
|
||||||
- if status.sensitive?
|
- if status.sensitive?
|
||||||
= render partial: 'stream_entries/content_spoiler'
|
= render partial: 'stream_entries/content_spoiler'
|
||||||
.status__attachments__inner
|
.status__attachments__inner<
|
||||||
- status.media_attachments.each do |media|
|
- status.media_attachments.each do |media|
|
||||||
= render partial: 'stream_entries/media', locals: { media: media }
|
= render partial: 'stream_entries/media', locals: { media: media }
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.media-item
|
.media-item><
|
||||||
= link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : '', target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do
|
= link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : '', target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do
|
||||||
- unless media.image?
|
- unless media.image?
|
||||||
%video{ src: media.file.url(:original), autoplay: true, loop: true }/
|
%video{ src: media.file.url(:original), autoplay: true, loop: true }/
|
||||||
|
|||||||
@@ -18,19 +18,20 @@
|
|||||||
%p{ style: 'margin-bottom: 0' }<
|
%p{ style: 'margin-bottom: 0' }<
|
||||||
%span.p-summary> #{status.spoiler_text}
|
%span.p-summary> #{status.spoiler_text}
|
||||||
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
|
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
|
||||||
.e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
|
.e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
|
||||||
|
= Formatter.instance.format(status)
|
||||||
|
|
||||||
- unless status.media_attachments.empty?
|
- unless status.media_attachments.empty?
|
||||||
.status__attachments
|
.status__attachments><
|
||||||
- if status.sensitive?
|
- if status.sensitive?
|
||||||
= render partial: 'stream_entries/content_spoiler'
|
= render partial: 'stream_entries/content_spoiler'
|
||||||
- if status.media_attachments.first.video?
|
- if status.media_attachments.first.video?
|
||||||
.status__attachments__inner
|
.status__attachments__inner<
|
||||||
.video-item
|
.video-item<
|
||||||
= link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do
|
= link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do
|
||||||
.video-item__play
|
.video-item__play
|
||||||
= fa_icon('play')
|
= fa_icon('play')
|
||||||
- else
|
- else
|
||||||
.status__attachments__inner
|
.status__attachments__inner<
|
||||||
- status.media_attachments.each do |media|
|
- status.media_attachments.each do |media|
|
||||||
= render partial: 'stream_entries/media', locals: { media: media }
|
= render partial: 'stream_entries/media', locals: { media: media }
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<p>Hello <%= @resource.email %>!</p>
|
<p>Hello <%= @resource.email %>!</p>
|
||||||
|
|
||||||
<p>We're contacting you to notify you that your password on Mastodon has been changed.</p>
|
<p>We're contacting you to notify you that your password on <%= @instance %> has been changed.</p>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
Hello <%= @resource.email %>!
|
Hello <%= @resource.email %>!
|
||||||
|
|
||||||
We're contacting you to notify you that your password on Mastodon has been changed.
|
We're contacting you to notify you that your password on <%= @instance %> has been changed.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<p>Witaj, <%= @resource.email %>!</p>
|
<p>Witaj, <%= @resource.email %>!</p>
|
||||||
|
|
||||||
<p>Informujemy, że ostatnio zmieniono Twoje hasło Mastodona.</p>
|
<p>Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>.</p>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
Witaj, <%= @resource.email %>!
|
Witaj, <%= @resource.email %>!
|
||||||
|
|
||||||
Informujemy, że ostatnio zmieniono Twoje hasło Mastodona.
|
Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<p>Hello <%= @resource.email %>!</p>
|
<p>Hello <%= @resource.email %>!</p>
|
||||||
|
|
||||||
<p>Someone has requested a link to change your password on Mastodon. You can do this through the link below.</p>
|
<p>Someone has requested a link to change your password on <%= @instance %>. You can do this through the link below.</p>
|
||||||
|
|
||||||
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
|
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Hello <%= @resource.email %>!
|
Hello <%= @resource.email %>!
|
||||||
|
|
||||||
Someone has requested a link to change your password on Mastodon. You can do this through the link below.
|
Someone has requested a link to change your password on <%= @instance %>. You can do this through the link below.
|
||||||
|
|
||||||
<%= edit_password_url(@resource, reset_password_token: @token) %>
|
<%= edit_password_url(@resource, reset_password_token: @token) %>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<p>Witaj, <%= @resource.email %>!</p>
|
<p>Witaj, <%= @resource.email %>!</p>
|
||||||
|
|
||||||
<p>Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link.</p>
|
<p>Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w
|
||||||
|
poniższy link.</p>
|
||||||
|
|
||||||
<p><%= link_to 'Zmień moje hasło', edit_password_url(@resource, reset_password_token: @token) %></p>
|
<p><%= link_to 'Zmień moje hasło', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
Witaj, <%= @resource.email %>!
|
Witaj, <%= @resource.email %>!
|
||||||
|
|
||||||
Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link.
|
Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w
|
||||||
|
poniższy link.
|
||||||
|
|
||||||
<%= edit_password_url(@resource, reset_password_token: @token) %>
|
<%= edit_password_url(@resource, reset_password_token: @token) %>
|
||||||
|
|
||||||
|
|||||||
11
app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
Normal file
11
app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
require 'sidekiq-scheduler'
|
||||||
|
|
||||||
|
class Scheduler::DoorkeeperCleanupScheduler
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
def perform
|
||||||
|
Doorkeeper::AccessToken.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
|
||||||
|
Doorkeeper::AccessGrant.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -67,17 +67,12 @@ module Mastodon
|
|||||||
|
|
||||||
config.active_job.queue_adapter = :sidekiq
|
config.active_job.queue_adapter = :sidekiq
|
||||||
|
|
||||||
#config.middleware.insert_before 0, Rack::Cors, debug: true, logger: (-> { Rails.logger }) do
|
|
||||||
config.middleware.insert_before 0, Rack::Cors do
|
config.middleware.insert_before 0, Rack::Cors do
|
||||||
allow do
|
allow do
|
||||||
origins '*'
|
origins '*'
|
||||||
resource '/@:username', headers: :any, methods: [:get], credentials: false
|
resource '/@:username', headers: :any, methods: [:get], credentials: false
|
||||||
resource '/api/*', headers: :any, methods: [:post, :put, :delete, :get, :patch, :options], credentials: false, expose: ['Link', 'X-RateLimit-Reset', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-Request-Id']
|
resource '/api/*', headers: :any, methods: [:post, :put, :delete, :get, :patch, :options], credentials: false, expose: ['Link', 'X-RateLimit-Reset', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-Request-Id']
|
||||||
resource '/oauth/token', headers: :any, methods: [:post], credentials: false
|
resource '/oauth/token', headers: :any, methods: [:post], credentials: false
|
||||||
resource '/assets/*', headers: :any, methods: [:get, :head, :options]
|
|
||||||
resource '/stylesheets/*', headers: :any, methods: [:get, :head, :options]
|
|
||||||
resource '/javascripts/*', headers: :any, methods: [:get, :head, :options]
|
|
||||||
resource '/packs/*', headers: :any, methods: [:get, :head, :options]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ default: &default
|
|||||||
development:
|
development:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: mastodon_development
|
database: mastodon_development
|
||||||
|
username: <%= ENV['DB_USER'] %>
|
||||||
|
password: <%= ENV['DB_PASS'] %>
|
||||||
|
host: <%= ENV['DB_HOST'] %>
|
||||||
|
port: <%= ENV['DB_PORT'] %>
|
||||||
|
|
||||||
# Warning: The database defined as "test" will be erased and
|
# Warning: The database defined as "test" will be erased and
|
||||||
# re-generated from your development database when you run "rake".
|
# re-generated from your development database when you run "rake".
|
||||||
@@ -14,6 +18,10 @@ development:
|
|||||||
test:
|
test:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: mastodon_test<%= ENV['TEST_ENV_NUMBER'] %>
|
database: mastodon_test<%= ENV['TEST_ENV_NUMBER'] %>
|
||||||
|
username: <%= ENV['DB_USER'] %>
|
||||||
|
password: <%= ENV['DB_PASS'] %>
|
||||||
|
host: <%= ENV['DB_HOST'] %>
|
||||||
|
port: <%= ENV['DB_PORT'] %>
|
||||||
|
|
||||||
production:
|
production:
|
||||||
<<: *default
|
<<: *default
|
||||||
|
|||||||
@@ -97,8 +97,5 @@ Rails.application.configure do
|
|||||||
'X-Frame-Options' => 'DENY',
|
'X-Frame-Options' => 'DENY',
|
||||||
'X-Content-Type-Options' => 'nosniff',
|
'X-Content-Type-Options' => 'nosniff',
|
||||||
'X-XSS-Protection' => '1; mode=block',
|
'X-XSS-Protection' => '1; mode=block',
|
||||||
'Content-Security-Policy' => "frame-ancestors 'none'; object-src 'none'; script-src 'self' https://dev-static.glitch.social 'unsafe-inline'; base-uri 'none';" ,
|
|
||||||
'Referrer-Policy' => 'no-referrer, strict-origin-when-cross-origin',
|
|
||||||
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload'
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Warden::Manager.after_set_user except: :fetch do |user, warden|
|
Warden::Manager.after_set_user except: :fetch do |user, warden|
|
||||||
SessionActivation.deactivate warden.raw_session['auth_id']
|
SessionActivation.deactivate warden.raw_session['auth_id']
|
||||||
warden.raw_session['auth_id'] = user.activate_session
|
warden.raw_session['auth_id'] = user.activate_session(warden.request)
|
||||||
end
|
end
|
||||||
|
|
||||||
Warden::Manager.after_fetch do |user, warden|
|
Warden::Manager.after_fetch do |user, warden|
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ ca:
|
|||||||
lost_recovery_codes: Els codis de recuperació et permeten recuperar l'accés al teu compte si perds el telèfon. Si has perdut els teus codis de recuperació els pots regenerar aquí. Els codis de recuperació anteriors seran anul·lats.
|
lost_recovery_codes: Els codis de recuperació et permeten recuperar l'accés al teu compte si perds el telèfon. Si has perdut els teus codis de recuperació els pots regenerar aquí. Els codis de recuperació anteriors seran anul·lats.
|
||||||
manual_instructions: 'Si no pots escanejar el codi QR code i necessites introduir-lo manualment, aquí tens el secret en text plà:'
|
manual_instructions: 'Si no pots escanejar el codi QR code i necessites introduir-lo manualment, aquí tens el secret en text plà:'
|
||||||
recovery_codes_regenerated: Codis de recuperació regenerats amb èxit
|
recovery_codes_regenerated: Codis de recuperació regenerats amb èxit
|
||||||
recovery_instructions: Si alguna vegada perds l'accéss al telèfon pots utilitzar un dels codis de recuperació a continuació per recuperar l'accés al teu compte. Cal mantenir els codis de recuperació en lloc segur, per exemple imprimint-los i guardar-los amb altres documents importants.
|
recovery_instructions_html: Si alguna vegada perds l'accéss al telèfon pots utilitzar un dels codis de recuperació a continuació per recuperar l'accés al teu compte. Cal mantenir els codis de recuperació en lloc segur, per exemple imprimint-los i guardar-los amb altres documents importants.
|
||||||
setup: Establir
|
setup: Establir
|
||||||
wrong_code: El codi introduït es invalid! Es correcta la hora del servidor i del dispositiu?
|
wrong_code: El codi introduït es invalid! Es correcta la hora del servidor i del dispositiu?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ de:
|
|||||||
lost_recovery_codes: Wiederherstellungscodes erlauben dir, wieder den Zugang zu deinem Konto zu erlangen, falls du dein Telefon verlierst. Wenn du deine Wiederherstellungscodes verloren hast, kannst du sie hier regenerieren. Deine alten Wiederherstellungscodes werden damit ungültig gemacht.
|
lost_recovery_codes: Wiederherstellungscodes erlauben dir, wieder den Zugang zu deinem Konto zu erlangen, falls du dein Telefon verlierst. Wenn du deine Wiederherstellungscodes verloren hast, kannst du sie hier regenerieren. Deine alten Wiederherstellungscodes werden damit ungültig gemacht.
|
||||||
manual_instructions: 'Wenn du den QR-Code nicht einlesen kannst und ihn manuell eingeben musst, ist hier das Klartext-Geheimnis:'
|
manual_instructions: 'Wenn du den QR-Code nicht einlesen kannst und ihn manuell eingeben musst, ist hier das Klartext-Geheimnis:'
|
||||||
recovery_codes_regenerated: Wiederherstellungscodes erfolgreich regeneriert
|
recovery_codes_regenerated: Wiederherstellungscodes erfolgreich regeneriert
|
||||||
recovery_instructions: Wenn du jemals den Zugang zu deinem Telefon verlierst, kannst du einen der Wiederherstellungscodes unten benutzen, um wieder auf dein Konto zugreifen zu können. Bewahre die Wiederherstellungscodes sicher auf, indem du sie beispielsweise ausdruckst und sie zusammen mit anderen wichtigen Dokumenten lagerst.
|
recovery_instructions_html: Wenn du jemals den Zugang zu deinem Telefon verlierst, kannst du einen der Wiederherstellungscodes unten benutzen, um wieder auf dein Konto zugreifen zu können. Bewahre die Wiederherstellungscodes sicher auf, indem du sie beispielsweise ausdruckst und sie zusammen mit anderen wichtigen Dokumenten lagerst.
|
||||||
setup: Einrichten
|
setup: Einrichten
|
||||||
wrong_code: Der eingegebene Code war ungültig! Sind die Server- und die Gerätezeit korrekt?
|
wrong_code: Der eingegebene Code war ungültig! Sind die Server- und die Gerätezeit korrekt?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ pl:
|
|||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Twój adres e-mail został poprawnie zweryfikowany.
|
confirmed: Twój adres e-mail został poprawnie zweryfikowany.
|
||||||
send_instructions: W ciągu kilku minut otrzymasz wiadomosć e-mail z instrukcją jak potwierdzić Twój adres e-mail.
|
send_instructions: W ciągu kilku minut otrzymasz wiadomosć e-mail z instrukcją jak potwierdzić Twój adres e-mail. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
|
||||||
send_paranoid_instructions: Jeśli Twój adres e-mail już istnieje w naszej bazie danych, w ciągu kilku minut otrzymasz wiadomość e-mail z instrukcją jak potwierdzić Twój adres e-mail.
|
send_paranoid_instructions: Jeśli Twój adres e-mail już istnieje w naszej bazie danych, w ciągu kilku minut otrzymasz wiadomość e-mail z instrukcją jak potwierdzić Twój adres e-mail. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
|
||||||
failure:
|
failure:
|
||||||
already_authenticated: Jesteś już zalogowany/zalogowana.
|
already_authenticated: Jesteś już zalogowany/zalogowana.
|
||||||
inactive: Twoje konto nie zostało jeszcze aktywowane.
|
inactive: Twoje konto nie zostało jeszcze aktywowane.
|
||||||
invalid: Błędne %{authentication_keys} lub hasło.
|
invalid: Nieprawidłowy %{authentication_keys} lub hasło.
|
||||||
last_attempt: Masz jeszcze jedną próbę; Twoje konto zostanie zablokowane jeśli się nie powiedzie.
|
last_attempt: Masz jeszcze jedną próbę; Twoje konto zostanie zablokowane jeśli się nie powiedzie.
|
||||||
locked: Twoje konto zostało zablokowane.
|
locked: Twoje konto zostało zablokowane.
|
||||||
not_found_in_database: Błędne %{authentication_keys} lub hasło.
|
not_found_in_database: Nieprawidłowy %{authentication_keys} lub hasło.
|
||||||
timeout: Twoja sesja wygasła. Zaloguj się ponownie aby kontynuować..
|
timeout: Twoja sesja wygasła. Zaloguj się ponownie aby kontynuować..
|
||||||
unauthenticated: Zapisz się lub zaloguj aby kontynuować.
|
unauthenticated: Zapisz się lub zaloguj aby kontynuować.
|
||||||
unconfirmed: Zweryfikuj adres e-mail aby kontynuować.
|
unconfirmed: Zweryfikuj adres e-mail aby kontynuować.
|
||||||
@@ -29,8 +29,8 @@ pl:
|
|||||||
success: Uwierzytelnienie przez %{kind} powiodło się.
|
success: Uwierzytelnienie przez %{kind} powiodło się.
|
||||||
passwords:
|
passwords:
|
||||||
no_token: Dostęp do tej strony możliwy jest wyłącznie za pomocą odnośnika z e-maila z instrukcjami ustawienia nowego hasła. Jeśli skorzystałeś/aś z takiego odnośnika, upewnij się, że został wykorzystany/skopiowany cały odnośnik.
|
no_token: Dostęp do tej strony możliwy jest wyłącznie za pomocą odnośnika z e-maila z instrukcjami ustawienia nowego hasła. Jeśli skorzystałeś/aś z takiego odnośnika, upewnij się, że został wykorzystany/skopiowany cały odnośnik.
|
||||||
send_instructions: W ciągu kilku minut otrzymasz wiadomość e-mail z instrukcją ustawienia nowego hasła.
|
send_instructions: W ciągu kilku minut otrzymasz wiadomość e-mail z instrukcją ustawienia nowego hasła. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
|
||||||
send_paranoid_instructions: Jeśli Twój adres e-mail już istnieje w naszej bazie danych, w ciągu kilku minut otrzymasz wiadomość e-mail zawierającą odnośnik pozwalający na ustawienie nowego hasła.
|
send_paranoid_instructions: Jeśli Twój adres e-mail już istnieje w naszej bazie danych, w ciągu kilku minut otrzymasz wiadomość e-mail zawierającą odnośnik pozwalający na ustawienie nowego hasła. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
|
||||||
updated: Twoje hasło zostało zmienione. Jesteś zalogowany/a.
|
updated: Twoje hasło zostało zmienione. Jesteś zalogowany/a.
|
||||||
updated_not_active: Twoje hasło zostało zmienione.
|
updated_not_active: Twoje hasło zostało zmienione.
|
||||||
registrations:
|
registrations:
|
||||||
@@ -38,16 +38,16 @@ pl:
|
|||||||
signed_up: Twoje konto zostało utworzone. Witamy!
|
signed_up: Twoje konto zostało utworzone. Witamy!
|
||||||
signed_up_but_inactive: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto nie zostało jeszcze aktywowane.
|
signed_up_but_inactive: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto nie zostało jeszcze aktywowane.
|
||||||
signed_up_but_locked: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto jest zablokowane.
|
signed_up_but_locked: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto jest zablokowane.
|
||||||
signed_up_but_unconfirmed: Na Twój adres e-mail została wysłana wiadomosć z odnośnikiem potwierdzającym. Kliknij w odnośnik aby aktywować konto.
|
signed_up_but_unconfirmed: Na Twój adres e-mail została wysłana wiadomosć z odnośnikiem potwierdzającym. Kliknij w odnośnik aby aktywować konto. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
|
||||||
update_needs_confirmation: Konto zostało zaktualizowane, musimy jednak zweryfikować Twój nowy adres e-mail. Została na niego wysłana wiadomość z odnośnikiem potwierdzającym.
|
update_needs_confirmation: Konto zostało zaktualizowane, musimy jednak zweryfikować Twój nowy adres e-mail. Została na niego wysłana wiadomość z odnośnikiem potwierdzającym. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
|
||||||
updated: Konto zostało zaktualizowane.
|
updated: Konto zostało zaktualizowane.
|
||||||
sessions:
|
sessions:
|
||||||
already_signed_out: Zostałeś/aś wylogowany/a.
|
already_signed_out: Zostałeś/aś wylogowany/a.
|
||||||
signed_in: Zostałeś/aś zalogowany/a.
|
signed_in: Zostałeś/aś zalogowany/a.
|
||||||
signed_out: Zostałeś/aś wylogowany/a.
|
signed_out: Zostałeś/aś wylogowany/a.
|
||||||
unlocks:
|
unlocks:
|
||||||
send_instructions: W ciągu kilku minut otrzymasz wiadomość e-mail z instrukcjami odblokowania konta.
|
send_instructions: W ciągu kilku minut otrzymasz wiadomość e-mail z instrukcjami odblokowania konta. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
|
||||||
send_paranoid_instructions: Jeśli Twoje konto istnieje, instrukcje odblokowania go otrzymasz w wiadomości e-mail w ciągu kilku minut.
|
send_paranoid_instructions: Jeśli Twoje konto istnieje, instrukcje odblokowania go otrzymasz w wiadomości e-mail w ciągu kilku minut. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
|
||||||
unlocked: Twoje konto zostało odblokowane. Zaloguj się aby kontynuować.
|
unlocked: Twoje konto zostało odblokowane. Zaloguj się aby kontynuować.
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
|
|||||||
@@ -193,6 +193,10 @@ en:
|
|||||||
title: PubSubHubbub
|
title: PubSubHubbub
|
||||||
topic: Topic
|
topic: Topic
|
||||||
title: Administration
|
title: Administration
|
||||||
|
admin_mailer:
|
||||||
|
new_report:
|
||||||
|
body: "%{reporter} has reported %{target}"
|
||||||
|
subject: New report for %{instance} (#%{id})
|
||||||
application_mailer:
|
application_mailer:
|
||||||
settings: 'Change e-mail preferences: %{link}'
|
settings: 'Change e-mail preferences: %{link}'
|
||||||
signature: Mastodon notifications from %{instance}
|
signature: Mastodon notifications from %{instance}
|
||||||
@@ -200,7 +204,7 @@ en:
|
|||||||
applications:
|
applications:
|
||||||
invalid_url: The provided URL is invalid
|
invalid_url: The provided URL is invalid
|
||||||
auth:
|
auth:
|
||||||
change_password: Credentials
|
change_password: Security
|
||||||
delete_account: Delete account
|
delete_account: Delete account
|
||||||
delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
|
delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
|
||||||
didnt_get_confirmation: Didn't receive confirmation instructions?
|
didnt_get_confirmation: Didn't receive confirmation instructions?
|
||||||
@@ -320,6 +324,43 @@ en:
|
|||||||
missing_resource: Could not find the required redirect URL for your account
|
missing_resource: Could not find the required redirect URL for your account
|
||||||
proceed: Proceed to follow
|
proceed: Proceed to follow
|
||||||
prompt: 'You are going to follow:'
|
prompt: 'You are going to follow:'
|
||||||
|
sessions:
|
||||||
|
activity: Last activity
|
||||||
|
browser: Browser
|
||||||
|
browsers:
|
||||||
|
alipay: Alipay
|
||||||
|
blackberry: Blackberry
|
||||||
|
chrome: Chrome
|
||||||
|
edge: Microsoft Edge
|
||||||
|
firefox: Firefox
|
||||||
|
generic: Unknown browser
|
||||||
|
ie: Internet Explorer
|
||||||
|
micro_messenger: MicroMessenger
|
||||||
|
nokia: Nokia S40 Ovi Browser
|
||||||
|
opera: Opera
|
||||||
|
phantom_js: PhantomJS
|
||||||
|
qq: QQ Browser
|
||||||
|
safari: Safari
|
||||||
|
uc_browser: UCBrowser
|
||||||
|
weibo: Weibo
|
||||||
|
current_session: Current session
|
||||||
|
description: "%{browser} on %{platform}"
|
||||||
|
explanation: These are the web browsers currently logged in to your Mastodon account.
|
||||||
|
ip: IP
|
||||||
|
platforms:
|
||||||
|
adobe_air: Adobe Air
|
||||||
|
android: Android
|
||||||
|
blackberry: Blackberry
|
||||||
|
chrome_os: ChromeOS
|
||||||
|
firefox_os: Firefox OS
|
||||||
|
ios: iOS
|
||||||
|
linux: Linux
|
||||||
|
mac: Mac
|
||||||
|
other: unknown platform
|
||||||
|
windows: Windows
|
||||||
|
windows_mobile: Windows Mobile
|
||||||
|
windows_phone: Windows Phone
|
||||||
|
title: Sessions
|
||||||
settings:
|
settings:
|
||||||
authorized_apps: Authorized apps
|
authorized_apps: Authorized apps
|
||||||
back: Back to Mastodon
|
back: Back to Mastodon
|
||||||
@@ -354,13 +395,15 @@ en:
|
|||||||
description_html: If you enable <strong>two-factor authentication</strong>, logging in will require you to be in possession of your phone, which will generate tokens for you to enter.
|
description_html: If you enable <strong>two-factor authentication</strong>, logging in will require you to be in possession of your phone, which will generate tokens for you to enter.
|
||||||
disable: Disable
|
disable: Disable
|
||||||
enable: Enable
|
enable: Enable
|
||||||
|
enabled: Two-factor authentication is enabled
|
||||||
enabled_success: Two-factor authentication successfully enabled
|
enabled_success: Two-factor authentication successfully enabled
|
||||||
generate_recovery_codes: Generate Recovery Codes
|
generate_recovery_codes: Generate recovery codes
|
||||||
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
|
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
|
||||||
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
|
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
|
||||||
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
|
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
|
||||||
|
recovery_codes: Backup recovery codes
|
||||||
recovery_codes_regenerated: Recovery codes successfully regenerated
|
recovery_codes_regenerated: Recovery codes successfully regenerated
|
||||||
recovery_instructions: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe. (For example, you may print them and store them with other important documents.)
|
recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents.
|
||||||
setup: Set up
|
setup: Set up
|
||||||
wrong_code: The entered code was invalid! Are server time and device time correct?
|
wrong_code: The entered code was invalid! Are server time and device time correct?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ fa:
|
|||||||
lost_recovery_codes: با کدهای بازیابی میتوانید اگر تلفن خود را گم کردید به حساب خود دسترسی داشته باشید. اگر کدهای بازیابی خود را گم کردید، آنها را اینجا دوباره بسازید. کدهای بازیابی قبلی شما نامعتبر خواهند شد.
|
lost_recovery_codes: با کدهای بازیابی میتوانید اگر تلفن خود را گم کردید به حساب خود دسترسی داشته باشید. اگر کدهای بازیابی خود را گم کردید، آنها را اینجا دوباره بسازید. کدهای بازیابی قبلی شما نامعتبر خواهند شد.
|
||||||
manual_instructions: 'اگر نمیتوانید کدها را اسکن کنید و باید آنها را دستی وارد کنید، متن کد امنیتی اینجاست:'
|
manual_instructions: 'اگر نمیتوانید کدها را اسکن کنید و باید آنها را دستی وارد کنید، متن کد امنیتی اینجاست:'
|
||||||
recovery_codes_regenerated: کدهای بازیابی با موفقیت ساخته شدند
|
recovery_codes_regenerated: کدهای بازیابی با موفقیت ساخته شدند
|
||||||
recovery_instructions: اگر تلفن خود را گم کردید، میتوانید با یکی از کدهای بازیابی زیر کنترل حساب خود را به دست بگیرید. این کدها را در جای امنی نگه دارید، مثلاً آنها را چاپ کنید و کنار سایر مدارک مهم خود قرار دهید
|
recovery_instructions_html: اگر تلفن خود را گم کردید، میتوانید با یکی از کدهای بازیابی زیر کنترل حساب خود را به دست بگیرید. این کدها را در جای امنی نگه دارید، مثلاً آنها را چاپ کنید و کنار سایر مدارک مهم خود قرار دهید
|
||||||
setup: راه اندازی
|
setup: راه اندازی
|
||||||
wrong_code: کدی که وارد کردید نامعتبر بود! آیا ساعت سرور و ساعت دستگاه شما درست تنظیم شدهاند؟
|
wrong_code: کدی که وارد کردید نامعتبر بود! آیا ساعت سرور و ساعت دستگاه شما درست تنظیم شدهاند؟
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ fr:
|
|||||||
mention: "%{name} vous a mentionné⋅e"
|
mention: "%{name} vous a mentionné⋅e"
|
||||||
new_followers_summary:
|
new_followers_summary:
|
||||||
one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi !
|
one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi !
|
||||||
other: Vous avez %{count} nouveaux abonné⋅es ! Incroyable !
|
other: Vous avez %{count} nouveaux⋅elles abonné⋅e⋅s ! Incroyable !
|
||||||
subject:
|
subject:
|
||||||
one: "Une nouvelle notification depuis votre dernière visite \U0001F418"
|
one: "Une nouvelle notification depuis votre dernière visite \U0001F418"
|
||||||
other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418"
|
other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418"
|
||||||
@@ -300,7 +300,7 @@ fr:
|
|||||||
lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés.
|
lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés.
|
||||||
manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l''entrer manuellement, voici le secret en clair :'
|
manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l''entrer manuellement, voici le secret en clair :'
|
||||||
recovery_codes_regenerated: Codes de récupération régénérés avec succès
|
recovery_codes_regenerated: Codes de récupération régénérés avec succès
|
||||||
recovery_instructions: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants.
|
recovery_instructions_html: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants.
|
||||||
setup: Installer
|
setup: Installer
|
||||||
wrong_code: Les codes entrés sont incorrects ! L'heure du serveur et celle de votre appareil sont-elles correctes ?
|
wrong_code: Les codes entrés sont incorrects ! L'heure du serveur et celle de votre appareil sont-elles correctes ?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ he:
|
|||||||
lost_recovery_codes: קודי האחזור מאפשרים אחזור גישה לחשבון במידה ומכשירך אבד. במידה וקודי האחזור אבדו, ניתן לייצרם מחדש כאן. תוקף קודי האחזור הישנים יפוג.
|
lost_recovery_codes: קודי האחזור מאפשרים אחזור גישה לחשבון במידה ומכשירך אבד. במידה וקודי האחזור אבדו, ניתן לייצרם מחדש כאן. תוקף קודי האחזור הישנים יפוג.
|
||||||
manual_instructions: 'במידה ולא ניתן לסרוק את קוד ה-QR אלא יש צורך להקליד אותו ידנית, להלן סוד כמוס בלתי מוצפן:'
|
manual_instructions: 'במידה ולא ניתן לסרוק את קוד ה-QR אלא יש צורך להקליד אותו ידנית, להלן סוד כמוס בלתי מוצפן:'
|
||||||
recovery_codes_regenerated: קודי האחזור יוצרו בהצלחה
|
recovery_codes_regenerated: קודי האחזור יוצרו בהצלחה
|
||||||
recovery_instructions: במידה והגישה למכשירך תאבד, ניתן לייצר קודי אחזור למטה על מנת לאחזר גישה לחשבונך בכל עת. נא לשמור על קודי הגישה במקום בטוח )לדוגמא על ידי הדפסתם ושמירתם עם מסמכים חשובים אחרים, או שימוש בתוכנה ייעודית לניהול סיסמאות וסודות(
|
recovery_instructions_html: במידה והגישה למכשירך תאבד, ניתן לייצר קודי אחזור למטה על מנת לאחזר גישה לחשבונך בכל עת. נא לשמור על קודי הגישה במקום בטוח )לדוגמא על ידי הדפסתם ושמירתם עם מסמכים חשובים אחרים, או שימוש בתוכנה ייעודית לניהול סיסמאות וסודות(
|
||||||
setup: הכנה
|
setup: הכנה
|
||||||
wrong_code: הקוד שהוזן שגוי! האם הזמן בשרת והזמן במכשירך נכונים?
|
wrong_code: הקוד שהוזן שגוי! האם הזמן בשרת והזמן במכשירך נכונים?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ id:
|
|||||||
lost_recovery_codes: Kode pemulihan bisa anda gunakan untuk mendapatkan kembali akses pada akun anda jika anda kehilangan handphone anda. Jika anda kehilangan kode pemulihan, anda bisa membuatnya ulang disini. Kode pemulihan anda yang lama tidak akan bisa digunakan lagi.
|
lost_recovery_codes: Kode pemulihan bisa anda gunakan untuk mendapatkan kembali akses pada akun anda jika anda kehilangan handphone anda. Jika anda kehilangan kode pemulihan, anda bisa membuatnya ulang disini. Kode pemulihan anda yang lama tidak akan bisa digunakan lagi.
|
||||||
manual_instructions: 'Jika anda tidak bisa memindai kode QR dan harus memasukkannya secara manual, ini dia kode yang harus dimasukkan:'
|
manual_instructions: 'Jika anda tidak bisa memindai kode QR dan harus memasukkannya secara manual, ini dia kode yang harus dimasukkan:'
|
||||||
recovery_codes_regenerated: Kode Pemulihan berhasil dibuat ulang
|
recovery_codes_regenerated: Kode Pemulihan berhasil dibuat ulang
|
||||||
recovery_instructions: Jika anda kehilangan akses pada handphone anda, anda bisa menggunakan kode pemulihan dibawah ini untuk mendapatkan kembali akses pada akun anda. Simpan kode pemulihan anda baik-baik, misalnya dengan mencetaknya atau menyimpannya bersama dokumen penting lainnya.
|
recovery_instructions_html: Jika anda kehilangan akses pada handphone anda, anda bisa menggunakan kode pemulihan dibawah ini untuk mendapatkan kembali akses pada akun anda. Simpan kode pemulihan anda baik-baik, misalnya dengan mencetaknya atau menyimpannya bersama dokumen penting lainnya.
|
||||||
setup: Persiapan
|
setup: Persiapan
|
||||||
wrong_code: Kode yang dimasukkan tidak cocok! Apa waktu server dan waktu di handphone sudah cocok?
|
wrong_code: Kode yang dimasukkan tidak cocok! Apa waktu server dan waktu di handphone sudah cocok?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ io:
|
|||||||
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
|
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
|
||||||
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
|
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
|
||||||
recovery_codes_regenerated: Recovery codes successfully regenerated
|
recovery_codes_regenerated: Recovery codes successfully regenerated
|
||||||
recovery_instructions: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe, for example by printing them and storing them with other important documents.
|
recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe, for example by printing them and storing them with other important documents.
|
||||||
setup: Set up
|
setup: Set up
|
||||||
wrong_code: The entered code was invalid! Are server time and device time correct?
|
wrong_code: The entered code was invalid! Are server time and device time correct?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ ja:
|
|||||||
lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。
|
lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。
|
||||||
manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:'
|
manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:'
|
||||||
recovery_codes_regenerated: リカバリーコードが再生成されました。
|
recovery_codes_regenerated: リカバリーコードが再生成されました。
|
||||||
recovery_instructions: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。 リカバリーコードは印刷して安全に保管してください。
|
recovery_instructions_html: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。 リカバリーコードは印刷して安全に保管してください。
|
||||||
setup: 初期設定
|
setup: 初期設定
|
||||||
wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。
|
wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ nl:
|
|||||||
lost_recovery_codes: Met herstelcodes kun je toegang tot jouw account krijgen wanneer je jouw telefoon bent kwijtgeraakt. Wanneer je jouw herstelcodes bent kwijtgeraakt, kan je ze hier opnieuw genereren. Jouw oude herstelcodes zijn daarna ongeldig.
|
lost_recovery_codes: Met herstelcodes kun je toegang tot jouw account krijgen wanneer je jouw telefoon bent kwijtgeraakt. Wanneer je jouw herstelcodes bent kwijtgeraakt, kan je ze hier opnieuw genereren. Jouw oude herstelcodes zijn daarna ongeldig.
|
||||||
manual_instructions: 'Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren.'
|
manual_instructions: 'Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren.'
|
||||||
recovery_codes_regenerated: Opnieuw genereren herstelcodes geslaagd
|
recovery_codes_regenerated: Opnieuw genereren herstelcodes geslaagd
|
||||||
recovery_instructions: Wanneer je ooit de toegang verliest tot jouw telefoon, kan je met behulp van een van de herstelcodes hieronder opnieuw toegang krijgen tot jouw account. Zorg ervoor dat je de herstelcodes op een veilige plek bewaard. (Je kunt ze bijvoorbeeld printen en ze samen met andere belangrijke documenten bewaren.)
|
recovery_instructions_html: Wanneer je ooit de toegang verliest tot jouw telefoon, kan je met behulp van een van de herstelcodes hieronder opnieuw toegang krijgen tot jouw account. Zorg ervoor dat je de herstelcodes op een veilige plek bewaard. (Je kunt ze bijvoorbeeld printen en ze samen met andere belangrijke documenten bewaren.)
|
||||||
setup: Instellen
|
setup: Instellen
|
||||||
wrong_code: De ingevoerde code is ongeldig! Klopt de systeemtijd van de server en die van jouw apparaat?
|
wrong_code: De ingevoerde code is ongeldig! Klopt de systeemtijd van de server en die van jouw apparaat?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -335,7 +335,7 @@
|
|||||||
lost_recovery_codes: Gjenopprettingskoder lar deg gjenoppnå tilgang til din konto hvis du mister din telefon. Hvis du har mistet gjenopprettingskodene, kan du regenerere dem her. Dine gamle gjenopprettingskoder vil bli ugyldige.
|
lost_recovery_codes: Gjenopprettingskoder lar deg gjenoppnå tilgang til din konto hvis du mister din telefon. Hvis du har mistet gjenopprettingskodene, kan du regenerere dem her. Dine gamle gjenopprettingskoder vil bli ugyldige.
|
||||||
manual_instructions: 'Hvis du ikke får scannet QR-koden må du skrive inn følgende kode manuelt:'
|
manual_instructions: 'Hvis du ikke får scannet QR-koden må du skrive inn følgende kode manuelt:'
|
||||||
recovery_codes_regenerated: Generering av gjenopprettingskoder vellykket
|
recovery_codes_regenerated: Generering av gjenopprettingskoder vellykket
|
||||||
recovery_instructions: Hvis du skulle miste tilgang til telefonen din, kan du bruke en av gjenopprettingskodene nedenfor til å gjenopprette tilgang til din konto. Oppbevar gjenopprettingskodene sikkert, for eksempel ved å skrive dem ut og lagre dem sammen med andre viktige dokumenter.
|
recovery_instructions_html: Hvis du skulle miste tilgang til telefonen din, kan du bruke en av gjenopprettingskodene nedenfor til å gjenopprette tilgang til din konto. Oppbevar gjenopprettingskodene sikkert, for eksempel ved å skrive dem ut og lagre dem sammen med andre viktige dokumenter.
|
||||||
setup: Sett opp
|
setup: Sett opp
|
||||||
wrong_code: Den angitte koden var ugyldig! Stemmer instansens tid overalt med enhetens tid?
|
wrong_code: Den angitte koden var ugyldig! Stemmer instansens tid overalt med enhetens tid?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ oc:
|
|||||||
lost_recovery_codes: Los còdi de recuperacion vos permeton d’accedir a vòstre compte se perdètz vòstre mobil. S’avètz perdut vòstres còdis de recuperacion los podètz tornar generar aquí. Los ancians còdis seràn pas mai valides.
|
lost_recovery_codes: Los còdi de recuperacion vos permeton d’accedir a vòstre compte se perdètz vòstre mobil. S’avètz perdut vòstres còdis de recuperacion los podètz tornar generar aquí. Los ancians còdis seràn pas mai valides.
|
||||||
manual_instructions: 'Se podètz pas numerizar lo còdi QR e que vos cal picar lo còdi a la man, vaquí lo còdi en clar :'
|
manual_instructions: 'Se podètz pas numerizar lo còdi QR e que vos cal picar lo còdi a la man, vaquí lo còdi en clar :'
|
||||||
recovery_codes_regenerated: Los còdis de recuperacion son ben estats tornats generar
|
recovery_codes_regenerated: Los còdis de recuperacion son ben estats tornats generar
|
||||||
recovery_instructions: Se vos arriba de perdre vòstre mobil, podètz utilizar un dels còdis de recuperacion cai-jos per poder tornar accedir a vòstre compte. Gardatz los còdis en seguretat, per exemple, imprimissètz los e gardatz los amb vòstres documents importants.
|
recovery_instructions_html: Se vos arriba de perdre vòstre mobil, podètz utilizar un dels còdis de recuperacion cai-jos per poder tornar accedir a vòstre compte. Gardatz los còdis en seguretat, per exemple, imprimissètz los e gardatz los amb vòstres documents importants.
|
||||||
setup: Paramètres
|
setup: Paramètres
|
||||||
wrong_code: Lo còdi picat es invalid ! L’ora es la bona sul servidor e lo mobil ?
|
wrong_code: Lo còdi picat es invalid ! L’ora es la bona sul servidor e lo mobil ?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -193,14 +193,18 @@ pl:
|
|||||||
title: PubSubHubbub
|
title: PubSubHubbub
|
||||||
topic: Temat
|
topic: Temat
|
||||||
title: Administracja
|
title: Administracja
|
||||||
|
admin_mailer:
|
||||||
|
new_report:
|
||||||
|
body: "Użytkownik %{reporter} zgłosił %{target}"
|
||||||
|
subject: Nowe zgłoszenie na %{instance} (#%{id})
|
||||||
application_mailer:
|
application_mailer:
|
||||||
settings: 'Zmień ustawienia powiadamiania: %{link}'
|
settings: 'Zmień ustawienia powiadamiania: %{link}'
|
||||||
signature: Powiadomienie Mastodona, wysłane przez %{instance}
|
signature: Powiadomienie Mastodona z instancji %{instance}
|
||||||
view: 'Zobacz:'
|
view: 'Zobacz:'
|
||||||
applications:
|
applications:
|
||||||
invalid_url: Ten URL jest nieprawidłowy
|
invalid_url: Ten URL jest nieprawidłowy
|
||||||
auth:
|
auth:
|
||||||
change_password: Uwierzytelnienie
|
change_password: Bezpieczeństwo
|
||||||
delete_account: Usunięcie konta
|
delete_account: Usunięcie konta
|
||||||
delete_account_html: Jeżeli próbowałeś usunąć konto, <a href="%{path}">przejdź tutaj</a>. Otrzymasz prośbę o potwierdzenie.
|
delete_account_html: Jeżeli próbowałeś usunąć konto, <a href="%{path}">przejdź tutaj</a>. Otrzymasz prośbę o potwierdzenie.
|
||||||
didnt_get_confirmation: Nie otrzymałeś instrukcji weryfikacji?
|
didnt_get_confirmation: Nie otrzymałeś instrukcji weryfikacji?
|
||||||
@@ -323,7 +327,44 @@ pl:
|
|||||||
acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić
|
acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić
|
||||||
missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny
|
missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny
|
||||||
proceed: Śledź
|
proceed: Śledź
|
||||||
prompt: 'Śledzony będzie:'
|
prompt: 'Zamierzasz śledzić:'
|
||||||
|
sessions:
|
||||||
|
activity: Ostatnia aktywność
|
||||||
|
browser: Przeglądarka
|
||||||
|
browsers:
|
||||||
|
alipay: Alipay
|
||||||
|
blackberry: Blackberry
|
||||||
|
chrome: Chrome
|
||||||
|
edge: Microsoft Edge
|
||||||
|
firefox: Firefox
|
||||||
|
generic: nieznana przeglądarka
|
||||||
|
ie: Internet Explorer
|
||||||
|
micro_messenger: MicroMessenger
|
||||||
|
nokia: Nokia S40 Ovi Browser
|
||||||
|
opera: Opera
|
||||||
|
phantom_js: PhantomJS
|
||||||
|
qq: QQ Browser
|
||||||
|
safari: Safari
|
||||||
|
uc_browser: UCBrowser
|
||||||
|
weibo: Weibo
|
||||||
|
current_session: Obecna sesja
|
||||||
|
description: "%{browser} na %{platform}"
|
||||||
|
explanation: Przeglądarki z aktywną sesją Twojego konta.
|
||||||
|
ip: Adres IP
|
||||||
|
platforms:
|
||||||
|
adobe_air: Adobe Air
|
||||||
|
android: Android
|
||||||
|
blackberry: Blackberry
|
||||||
|
chrome_os: ChromeOS
|
||||||
|
firefox_os: Firefox OS
|
||||||
|
ios: iOS
|
||||||
|
linux: Linux
|
||||||
|
mac: macOS
|
||||||
|
other: nieznana platforma
|
||||||
|
windows: Windows
|
||||||
|
windows_mobile: Windows Mobile
|
||||||
|
windows_phone: Windows Phone
|
||||||
|
title: Sesje
|
||||||
settings:
|
settings:
|
||||||
authorized_apps: Uwierzytelnione aplikacje
|
authorized_apps: Uwierzytelnione aplikacje
|
||||||
back: Powrót do Mastodona
|
back: Powrót do Mastodona
|
||||||
@@ -358,13 +399,15 @@ pl:
|
|||||||
description_html: Jeśli włączysz <strong>uwierzytelnianie dwuetapowe</strong>, logowanie się będzie wymagało podania tokenu wyświetlonego na Twoim telefonie.
|
description_html: Jeśli włączysz <strong>uwierzytelnianie dwuetapowe</strong>, logowanie się będzie wymagało podania tokenu wyświetlonego na Twoim telefonie.
|
||||||
disable: Wyłącz
|
disable: Wyłącz
|
||||||
enable: Włącz
|
enable: Włącz
|
||||||
|
enabled: Uwierzytelnianie dwuetapowe jest włączone
|
||||||
enabled_success: Pomyślnie aktywowano uwierzytelnianie dwuetapowe
|
enabled_success: Pomyślnie aktywowano uwierzytelnianie dwuetapowe
|
||||||
generate_recovery_codes: Generuj kody zapasowe
|
generate_recovery_codes: Generuj kody zapasowe
|
||||||
instructions_html: "<strong>Zeskanuj ten kod QR na swoim urządzeniu za pomocą Google Authenticator, FreeOTP lub podobnej aplikacji</strong>. Od teraz będzie ona generowała kody wymagane przy logowaniu."
|
instructions_html: "<strong>Zeskanuj ten kod QR na swoim urządzeniu za pomocą Google Authenticator, FreeOTP lub podobnej aplikacji</strong>. Od teraz będzie ona generowała kody wymagane przy logowaniu."
|
||||||
lost_recovery_codes: Kody zapasowe pozwolą uzyskać dostęp do portalu, jeżeli utracisz dostęp do telefonu. Jeżeli utracisz dostęp do nich, możesz wygenerować je ponownie tutaj. Poprzednie zostaną unieważnione.
|
lost_recovery_codes: Kody zapasowe pozwolą uzyskać dostęp do portalu, jeżeli utracisz dostęp do telefonu. Jeżeli utracisz dostęp do nich, możesz wygenerować je ponownie tutaj. Poprzednie zostaną unieważnione.
|
||||||
manual_instructions: 'Jeżeli nie możesz zeskanować kodu QR, musisz wprowadzić ten kod ręcznie:'
|
manual_instructions: 'Jeżeli nie możesz zeskanować kodu QR, musisz wprowadzić ten kod ręcznie:'
|
||||||
|
recovery_codes: Przywróć kody zapasowe
|
||||||
recovery_codes_regenerated: Pomyślnie wygenerowano ponownie kody zapasowe
|
recovery_codes_regenerated: Pomyślnie wygenerowano ponownie kody zapasowe
|
||||||
recovery_instructions: Jeżeli kiedykolwiek utracisz dostęp do telefonu, możesz wykorzystać jeden z kodów zapasowych, aby odzyskać dostęp do konta. Trzymaj je w bezpiecznym miejscu. (Na przykład, wydrukuj je i przechowuj z ważnymu dokumentami.)
|
recovery_instructions_html: Jeżeli kiedykolwiek utracisz dostęp do telefonu, możesz wykorzystać jeden z kodów zapasowych, aby odzyskać dostęp do konta. <strong>Trzymaj je w bezpiecznym miejscu</strong>. Na przykład, wydrukuj je i przechowuj z ważnymu dokumentami.
|
||||||
setup: Skonfiguruj
|
setup: Skonfiguruj
|
||||||
wrong_code: Wprowadzony kod jest niepoprawny! Czy czas serwera i urządzenia jest poprawny?
|
wrong_code: Wprowadzony kod jest niepoprawny! Czy czas serwera i urządzenia jest poprawny?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ pt-BR:
|
|||||||
lost_recovery_codes: Códigos de recuperação permite que você recupere o acesso a sua conta se você perder seu telefone. Se você perder os códigos de recuperação, você pode regera-los aqui. Seus códigos antigos serão invalidados.
|
lost_recovery_codes: Códigos de recuperação permite que você recupere o acesso a sua conta se você perder seu telefone. Se você perder os códigos de recuperação, você pode regera-los aqui. Seus códigos antigos serão invalidados.
|
||||||
manual_instructions: 'Se você não puder scanear o código QR e precisa digita-los manualmente, aqui está o segredo em texto.:'
|
manual_instructions: 'Se você não puder scanear o código QR e precisa digita-los manualmente, aqui está o segredo em texto.:'
|
||||||
recovery_codes_regenerated: Códigos de recuperação foram gerados com sucesso
|
recovery_codes_regenerated: Códigos de recuperação foram gerados com sucesso
|
||||||
recovery_instructions: Se algum dia você perder o acesso ao seu telefone, você pode usar um dos códigos de abaixo para recupera o acesso a sua conta. Guarde os códigos de acesso em local seguro, por exemplo imprimindo ou guardados com documentos importantes.
|
recovery_instructions_html: Se algum dia você perder o acesso ao seu telefone, você pode usar um dos códigos de abaixo para recupera o acesso a sua conta. Guarde os códigos de acesso em local seguro, por exemplo imprimindo ou guardados com documentos importantes.
|
||||||
setup: Configurar
|
setup: Configurar
|
||||||
wrong_code: O código digitado é inválido! Os relógios do servidor e do dispositivo estão corretos?
|
wrong_code: O código digitado é inválido! Os relógios do servidor e do dispositivo estão corretos?
|
||||||
users:
|
users:
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ ru:
|
|||||||
lost_recovery_codes: Коды восстановления позволяют вернуть доступ к аккаунту в случае утери телефона. Если Вы потеряли Ваши коды восстановления, вы можете заново сгенерировать их здесь. Ваши старые коды восстановления будут аннулированы.
|
lost_recovery_codes: Коды восстановления позволяют вернуть доступ к аккаунту в случае утери телефона. Если Вы потеряли Ваши коды восстановления, вы можете заново сгенерировать их здесь. Ваши старые коды восстановления будут аннулированы.
|
||||||
manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
|
manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
|
||||||
recovery_codes_regenerated: Коды восстановления успешно сгенерированы
|
recovery_codes_regenerated: Коды восстановления успешно сгенерированы
|
||||||
recovery_instructions: В случае утери доступа к Вашему телефону Вы можете использовать один из кодов восстановления, указанных ниже, чтобы вернуть доступ к аккаунту. Держите коды восстановления в безопасности, например, распечатав их и храня с другими важными документами.
|
recovery_instructions_html: В случае утери доступа к Вашему телефону Вы можете использовать один из кодов восстановления, указанных ниже, чтобы вернуть доступ к аккаунту. Держите коды восстановления в безопасности, например, распечатав их и храня с другими важными документами.
|
||||||
setup: Настроить
|
setup: Настроить
|
||||||
wrong_code: Введенный код неверен! Правильно ли установлены серверное время и время устройства?
|
wrong_code: Введенный код неверен! Правильно ли установлены серверное время и время устройства?
|
||||||
users:
|
users:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user