merge messaging

This commit is contained in:
tykayn 2020-02-01 12:17:06 +01:00
commit 519c9f417e
166 changed files with 3234 additions and 1885 deletions

1
.gitignore vendored
View File

@ -13,6 +13,7 @@
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
.eslintcache
/log/*
!/log/.keep
/tmp

View File

@ -3,6 +3,7 @@ Authors
Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon)
and provided thanks to the work of the following contributors:
* [Tykayn](https://framagit.org/tykayn) for the Bliss version
* [Gargron](https://github.com/Gargron)
* [ThibG](https://github.com/ThibG)

View File

@ -6,7 +6,8 @@ ruby '>= 2.4.0', '< 2.7.0'
gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 4.3'
gem 'rails', '~> 5.2.3'
gem 'rails', '~> 5.2.4'
gem 'sprockets', '~> 3.7'
gem 'thor', '~> 0.20'
gem 'hamlit-rails', '~> 0.2'
@ -122,7 +123,7 @@ group :test do
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.17', require: false
gem 'webmock', '~> 3.7'
gem 'parallel_tests', '~> 2.29'
gem 'parallel_tests', '~> 2.30'
end
group :development do

View File

@ -44,25 +44,25 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (5.2.3)
actionpack (= 5.2.3)
actioncable (5.2.4)
actionpack (= 5.2.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
activejob (= 5.2.3)
actionmailer (5.2.4)
actionpack (= 5.2.4)
actionview (= 5.2.4)
activejob (= 5.2.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.3)
actionview (= 5.2.3)
activesupport (= 5.2.3)
actionpack (5.2.4)
actionview (= 5.2.4)
activesupport (= 5.2.4)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.3)
activesupport (= 5.2.3)
actionview (5.2.4)
activesupport (= 5.2.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -73,20 +73,20 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.7)
activejob (5.2.3)
activesupport (= 5.2.3)
activejob (5.2.4)
activesupport (= 5.2.4)
globalid (>= 0.3.6)
activemodel (5.2.3)
activesupport (= 5.2.3)
activerecord (5.2.3)
activemodel (= 5.2.3)
activesupport (= 5.2.3)
activemodel (5.2.4)
activesupport (= 5.2.4)
activerecord (5.2.4)
activemodel (= 5.2.4)
activesupport (= 5.2.4)
arel (>= 9.0)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
activestorage (5.2.4)
actionpack (= 5.2.4)
activerecord (= 5.2.4)
marcel (~> 0.3.1)
activesupport (5.2.3)
activesupport (5.2.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -218,7 +218,7 @@ GEM
docile (1.3.2)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.2.2)
doorkeeper (5.2.3)
railties (>= 5)
dotenv (2.7.5)
dotenv-rails (2.7.5)
@ -238,9 +238,9 @@ GEM
erubi (1.9.0)
et-orbi (1.1.6)
tzinfo
excon (0.62.0)
excon (0.71.0)
fabrication (2.21.0)
faker (2.8.0)
faker (2.8.1)
i18n (>= 1.6, < 1.8)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
@ -324,7 +324,7 @@ GEM
jmespath (1.4.0)
json (2.2.0)
json-canonicalization (0.1.0)
json-ld-preloaded (3.0.4)
json-ld-preloaded (3.0.6)
json-ld (~> 3.0)
multi_json (~> 1.12)
rdf (~> 3.0)
@ -380,7 +380,7 @@ GEM
mini_portile2 (2.4.0)
minitest (5.13.0)
msgpack (1.3.1)
multi_json (1.13.1)
multi_json (1.14.1)
multipart-post (2.1.1)
necromancer (0.5.1)
net-ldap (0.16.2)
@ -424,7 +424,7 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.19.1)
parallel_tests (2.29.2)
parallel_tests (2.30.0)
parallel
parser (2.6.5.0)
ast (~> 2.4.0)
@ -469,18 +469,18 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.3)
actioncable (= 5.2.3)
actionmailer (= 5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
activejob (= 5.2.3)
activemodel (= 5.2.3)
activerecord (= 5.2.3)
activestorage (= 5.2.3)
activesupport (= 5.2.3)
rails (5.2.4)
actioncable (= 5.2.4)
actionmailer (= 5.2.4)
actionpack (= 5.2.4)
actionview (= 5.2.4)
activejob (= 5.2.4)
activemodel (= 5.2.4)
activerecord (= 5.2.4)
activestorage (= 5.2.4)
activesupport (= 5.2.4)
bundler (>= 1.3.0)
railties (= 5.2.3)
railties (= 5.2.4)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x)
@ -496,15 +496,15 @@ GEM
railties (>= 5.0, < 6)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
railties (5.2.3)
actionpack (= 5.2.3)
activesupport (= 5.2.3)
railties (5.2.4)
actionpack (= 5.2.4)
activesupport (= 5.2.4)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
rake (13.0.1)
rdf (3.0.12)
rdf (3.0.13)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.3)
@ -615,7 +615,7 @@ GEM
sshkit (1.20.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.13)
stackprof (0.2.14)
statsd-ruby (1.4.0)
stoplight (2.2.0)
streamio-ffmpeg (3.0.2)
@ -667,9 +667,9 @@ GEM
webpush (0.3.8)
hkdf (~> 0.2)
jwt (~> 2.0)
websocket-driver (0.7.0)
websocket-driver (0.7.1)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
websocket-extensions (0.1.4)
wisper (2.0.1)
xpath (3.2.0)
nokogiri (~> 1.8)
@ -753,7 +753,7 @@ DEPENDENCIES
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel (~> 1.19)
parallel_tests (~> 2.29)
parallel_tests (~> 2.30)
parslet
pg (~> 1.1)
pghero (~> 2.4)
@ -767,7 +767,7 @@ DEPENDENCIES
pundit (~> 2.1)
rack-attack (~> 6.2)
rack-cors (~> 1.1)
rails (~> 5.2.3)
rails (~> 5.2.4)
rails-controller-testing (~> 1.0)
rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6)
@ -789,6 +789,7 @@ DEPENDENCIES
simple-navigation (~> 4.1)
simple_form (~> 5.0)
simplecov (~> 0.17)
sprockets (~> 3.7)
sprockets-rails (~> 3.2)
stackprof
stoplight (~> 2.2.0)

View File

@ -1,5 +1,23 @@
![Mastodon](https://i.imgur.com/NhZc40l.png)
========
# CipherBliss version of Mastodon
features:
* liks to free tools
* snow fall during the end of the yar.
## Todo
* proper responsive columning
* **instant Messaging** in the web front end.
uses the api to get accounts
```bash
GET /api/v1/accounts/[id of my account]/following
```
========
[![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci]
@ -72,7 +90,7 @@ Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Strea
- **Ruby** 2.4+
- **Node.js** 8+
The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/administration/installation/) is available in the documentation.
The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
A **Vagrant** configuration is included for development purposes.

View File

@ -4,75 +4,75 @@ import { importAccount, importFetchedAccount, importFetchedAccounts } from './im
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS';
export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS';
export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST';
export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS';
export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL';
export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL';
export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL';
export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL';
export const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST';
export const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS';
export const FOLLOWERS_EXPAND_FAIL = 'FOLLOWERS_EXPAND_FAIL';
export const FOLLOWERS_EXPAND_FAIL = 'FOLLOWERS_EXPAND_FAIL';
export const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST';
export const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS';
export const FOLLOWING_FETCH_FAIL = 'FOLLOWING_FETCH_FAIL';
export const FOLLOWING_FETCH_FAIL = 'FOLLOWING_FETCH_FAIL';
export const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST';
export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
export const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS';
export const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL';
export const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL';
export const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST';
export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
function getFromDB(dispatch, getState, index, id) {
return new Promise((resolve, reject) => {
@ -106,7 +106,7 @@ export function fetchAccount(id) {
dispatch,
getState,
db.transaction('accounts', 'read').objectStore('accounts').index('id'),
id
id,
).then(() => db.close(), error => {
db.close();
throw error;
@ -118,29 +118,29 @@ export function fetchAccount(id) {
dispatch(fetchAccountFail(id, error));
});
};
};
}
export function fetchAccountRequest(id) {
return {
type: ACCOUNT_FETCH_REQUEST,
id,
};
};
}
export function fetchAccountSuccess() {
return {
type: ACCOUNT_FETCH_SUCCESS,
};
};
}
export function fetchAccountFail(id, error) {
return {
type: ACCOUNT_FETCH_FAIL,
type : ACCOUNT_FETCH_FAIL,
id,
error,
skipAlert: true,
};
};
}
export function followAccount(id, reblogs = true) {
return (dispatch, getState) => {
@ -155,7 +155,7 @@ export function followAccount(id, reblogs = true) {
dispatch(followAccountFail(error, locked));
});
};
};
}
export function unfollowAccount(id) {
return (dispatch, getState) => {
@ -167,59 +167,59 @@ export function unfollowAccount(id) {
dispatch(unfollowAccountFail(error));
});
};
};
}
export function followAccountRequest(id, locked) {
return {
type: ACCOUNT_FOLLOW_REQUEST,
type : ACCOUNT_FOLLOW_REQUEST,
id,
locked,
skipLoading: true,
};
};
}
export function followAccountSuccess(relationship, alreadyFollowing) {
return {
type: ACCOUNT_FOLLOW_SUCCESS,
type : ACCOUNT_FOLLOW_SUCCESS,
relationship,
alreadyFollowing,
skipLoading: true,
};
};
}
export function followAccountFail(error, locked) {
return {
type: ACCOUNT_FOLLOW_FAIL,
type : ACCOUNT_FOLLOW_FAIL,
error,
locked,
skipLoading: true,
};
};
}
export function unfollowAccountRequest(id) {
return {
type: ACCOUNT_UNFOLLOW_REQUEST,
type : ACCOUNT_UNFOLLOW_REQUEST,
id,
skipLoading: true,
};
};
}
export function unfollowAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_UNFOLLOW_SUCCESS,
type : ACCOUNT_UNFOLLOW_SUCCESS,
relationship,
statuses,
skipLoading: true,
};
};
}
export function unfollowAccountFail(error) {
return {
type: ACCOUNT_UNFOLLOW_FAIL,
type : ACCOUNT_UNFOLLOW_FAIL,
error,
skipLoading: true,
};
};
}
export function blockAccount(id) {
return (dispatch, getState) => {
@ -232,7 +232,7 @@ export function blockAccount(id) {
dispatch(blockAccountFail(id, error));
});
};
};
}
export function unblockAccount(id) {
return (dispatch, getState) => {
@ -244,14 +244,14 @@ export function unblockAccount(id) {
dispatch(unblockAccountFail(id, error));
});
};
};
}
export function blockAccountRequest(id) {
return {
type: ACCOUNT_BLOCK_REQUEST,
id,
};
};
}
export function blockAccountSuccess(relationship, statuses) {
return {
@ -259,36 +259,35 @@ export function blockAccountSuccess(relationship, statuses) {
relationship,
statuses,
};
};
}
export function blockAccountFail(error) {
return {
type: ACCOUNT_BLOCK_FAIL,
error,
};
};
}
export function unblockAccountRequest(id) {
return {
type: ACCOUNT_UNBLOCK_REQUEST,
id,
};
};
}
export function unblockAccountSuccess(relationship) {
return {
type: ACCOUNT_UNBLOCK_SUCCESS,
relationship,
};
};
}
export function unblockAccountFail(error) {
return {
type: ACCOUNT_UNBLOCK_FAIL,
error,
};
};
}
export function muteAccount(id, notifications) {
return (dispatch, getState) => {
@ -301,7 +300,7 @@ export function muteAccount(id, notifications) {
dispatch(muteAccountFail(id, error));
});
};
};
}
export function unmuteAccount(id) {
return (dispatch, getState) => {
@ -313,14 +312,14 @@ export function unmuteAccount(id) {
dispatch(unmuteAccountFail(id, error));
});
};
};
}
export function muteAccountRequest(id) {
return {
type: ACCOUNT_MUTE_REQUEST,
id,
};
};
}
export function muteAccountSuccess(relationship, statuses) {
return {
@ -328,36 +327,35 @@ export function muteAccountSuccess(relationship, statuses) {
relationship,
statuses,
};
};
}
export function muteAccountFail(error) {
return {
type: ACCOUNT_MUTE_FAIL,
error,
};
};
}
export function unmuteAccountRequest(id) {
return {
type: ACCOUNT_UNMUTE_REQUEST,
id,
};
};
}
export function unmuteAccountSuccess(relationship) {
return {
type: ACCOUNT_UNMUTE_SUCCESS,
relationship,
};
};
}
export function unmuteAccountFail(error) {
return {
type: ACCOUNT_UNMUTE_FAIL,
error,
};
};
}
export function fetchFollowers(id) {
return (dispatch, getState) => {
@ -373,14 +371,14 @@ export function fetchFollowers(id) {
dispatch(fetchFollowersFail(id, error));
});
};
};
}
export function fetchFollowersRequest(id) {
return {
type: FOLLOWERS_FETCH_REQUEST,
id,
};
};
}
export function fetchFollowersSuccess(id, accounts, next) {
return {
@ -389,7 +387,7 @@ export function fetchFollowersSuccess(id, accounts, next) {
accounts,
next,
};
};
}
export function fetchFollowersFail(id, error) {
return {
@ -397,7 +395,7 @@ export function fetchFollowersFail(id, error) {
id,
error,
};
};
}
export function expandFollowers(id) {
return (dispatch, getState) => {
@ -419,14 +417,14 @@ export function expandFollowers(id) {
dispatch(expandFollowersFail(id, error));
});
};
};
}
export function expandFollowersRequest(id) {
return {
type: FOLLOWERS_EXPAND_REQUEST,
id,
};
};
}
export function expandFollowersSuccess(id, accounts, next) {
return {
@ -435,7 +433,7 @@ export function expandFollowersSuccess(id, accounts, next) {
accounts,
next,
};
};
}
export function expandFollowersFail(id, error) {
return {
@ -443,7 +441,7 @@ export function expandFollowersFail(id, error) {
id,
error,
};
};
}
export function fetchFollowing(id) {
return (dispatch, getState) => {
@ -459,14 +457,14 @@ export function fetchFollowing(id) {
dispatch(fetchFollowingFail(id, error));
});
};
};
}
export function fetchFollowingRequest(id) {
return {
type: FOLLOWING_FETCH_REQUEST,
id,
};
};
}
export function fetchFollowingSuccess(id, accounts, next) {
return {
@ -475,7 +473,7 @@ export function fetchFollowingSuccess(id, accounts, next) {
accounts,
next,
};
};
}
export function fetchFollowingFail(id, error) {
return {
@ -483,7 +481,7 @@ export function fetchFollowingFail(id, error) {
id,
error,
};
};
}
export function expandFollowing(id) {
return (dispatch, getState) => {
@ -505,14 +503,14 @@ export function expandFollowing(id) {
dispatch(expandFollowingFail(id, error));
});
};
};
}
export function expandFollowingRequest(id) {
return {
type: FOLLOWING_EXPAND_REQUEST,
id,
};
};
}
export function expandFollowingSuccess(id, accounts, next) {
return {
@ -521,7 +519,7 @@ export function expandFollowingSuccess(id, accounts, next) {
accounts,
next,
};
};
}
export function expandFollowingFail(id, error) {
return {
@ -529,7 +527,7 @@ export function expandFollowingFail(id, error) {
id,
error,
};
};
}
export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
@ -548,31 +546,31 @@ export function fetchRelationships(accountIds) {
dispatch(fetchRelationshipsFail(error));
});
};
};
}
export function fetchRelationshipsRequest(ids) {
return {
type: RELATIONSHIPS_FETCH_REQUEST,
type : RELATIONSHIPS_FETCH_REQUEST,
ids,
skipLoading: true,
};
};
}
export function fetchRelationshipsSuccess(relationships) {
return {
type: RELATIONSHIPS_FETCH_SUCCESS,
type : RELATIONSHIPS_FETCH_SUCCESS,
relationships,
skipLoading: true,
};
};
}
export function fetchRelationshipsFail(error) {
return {
type: RELATIONSHIPS_FETCH_FAIL,
type : RELATIONSHIPS_FETCH_FAIL,
error,
skipLoading: true,
};
};
}
export function fetchFollowRequests() {
return (dispatch, getState) => {
@ -584,13 +582,13 @@ export function fetchFollowRequests() {
dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
}).catch(error => dispatch(fetchFollowRequestsFail(error)));
};
};
}
export function fetchFollowRequestsRequest() {
return {
type: FOLLOW_REQUESTS_FETCH_REQUEST,
};
};
}
export function fetchFollowRequestsSuccess(accounts, next) {
return {
@ -598,14 +596,14 @@ export function fetchFollowRequestsSuccess(accounts, next) {
accounts,
next,
};
};
}
export function fetchFollowRequestsFail(error) {
return {
type: FOLLOW_REQUESTS_FETCH_FAIL,
error,
};
};
}
export function expandFollowRequests() {
return (dispatch, getState) => {
@ -623,13 +621,13 @@ export function expandFollowRequests() {
dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
}).catch(error => dispatch(expandFollowRequestsFail(error)));
};
};
}
export function expandFollowRequestsRequest() {
return {
type: FOLLOW_REQUESTS_EXPAND_REQUEST,
};
};
}
export function expandFollowRequestsSuccess(accounts, next) {
return {
@ -637,14 +635,14 @@ export function expandFollowRequestsSuccess(accounts, next) {
accounts,
next,
};
};
}
export function expandFollowRequestsFail(error) {
return {
type: FOLLOW_REQUESTS_EXPAND_FAIL,
error,
};
};
}
export function authorizeFollowRequest(id) {
return (dispatch, getState) => {
@ -655,21 +653,21 @@ export function authorizeFollowRequest(id) {
.then(() => dispatch(authorizeFollowRequestSuccess(id)))
.catch(error => dispatch(authorizeFollowRequestFail(id, error)));
};
};
}
export function authorizeFollowRequestRequest(id) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_REQUEST,
id,
};
};
}
export function authorizeFollowRequestSuccess(id) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
id,
};
};
}
export function authorizeFollowRequestFail(id, error) {
return {
@ -677,8 +675,7 @@ export function authorizeFollowRequestFail(id, error) {
id,
error,
};
};
}
export function rejectFollowRequest(id) {
return (dispatch, getState) => {
@ -689,21 +686,21 @@ export function rejectFollowRequest(id) {
.then(() => dispatch(rejectFollowRequestSuccess(id)))
.catch(error => dispatch(rejectFollowRequestFail(id, error)));
};
};
}
export function rejectFollowRequestRequest(id) {
return {
type: FOLLOW_REQUEST_REJECT_REQUEST,
id,
};
};
}
export function rejectFollowRequestSuccess(id) {
return {
type: FOLLOW_REQUEST_REJECT_SUCCESS,
id,
};
};
}
export function rejectFollowRequestFail(id, error) {
return {
@ -711,7 +708,7 @@ export function rejectFollowRequestFail(id, error) {
id,
error,
};
};
}
export function pinAccount(id) {
return (dispatch, getState) => {
@ -723,7 +720,7 @@ export function pinAccount(id) {
dispatch(pinAccountFail(error));
});
};
};
}
export function unpinAccount(id) {
return (dispatch, getState) => {
@ -735,46 +732,46 @@ export function unpinAccount(id) {
dispatch(unpinAccountFail(error));
});
};
};
}
export function pinAccountRequest(id) {
return {
type: ACCOUNT_PIN_REQUEST,
id,
};
};
}
export function pinAccountSuccess(relationship) {
return {
type: ACCOUNT_PIN_SUCCESS,
relationship,
};
};
}
export function pinAccountFail(error) {
return {
type: ACCOUNT_PIN_FAIL,
error,
};
};
}
export function unpinAccountRequest(id) {
return {
type: ACCOUNT_UNPIN_REQUEST,
id,
};
};
}
export function unpinAccountSuccess(relationship) {
return {
type: ACCOUNT_UNPIN_SUCCESS,
relationship,
};
};
}
export function unpinAccountFail(error) {
return {
type: ACCOUNT_UNPIN_FAIL,
error,
};
};
}

View File

@ -7,26 +7,25 @@ import { useEmoji } from './emojis';
import resizeImage from '../utils/resize_image';
import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines';
import { showAlertForError } from './alerts';
import { showAlert } from './alerts';
import { showAlert, showAlertForError } from './alerts';
import { defineMessages } from 'react-intl';
let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags;
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
@ -35,32 +34,32 @@ export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD';
export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE';
export const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD';
export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD';
export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE';
export const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD';
export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
uploadErrorPoll : { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
});
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
@ -76,57 +75,57 @@ export function changeCompose(text) {
type: COMPOSE_CHANGE,
text: text,
};
};
}
export function replyCompose(status, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_REPLY,
type : COMPOSE_REPLY,
status: status,
});
ensureComposeIsVisible(getState, routerHistory);
};
};
}
export function cancelReplyCompose() {
return {
type: COMPOSE_REPLY_CANCEL,
};
};
}
export function resetCompose() {
return {
type: COMPOSE_RESET,
};
};
}
export function mentionCompose(account, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_MENTION,
type : COMPOSE_MENTION,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
};
};
}
export function directCompose(account, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_DIRECT,
type : COMPOSE_DIRECT,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
};
};
}
export function submitCompose(routerHistory) {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']);
if ((!status || !status.length) && media.size === 0) {
return;
@ -137,11 +136,11 @@ export function submitCompose(routerHistory) {
api(getState).post('/api/v1/statuses', {
status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: media.map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
media_ids : media.map(item => item.get('id')),
sensitive : getState().getIn(['compose', 'sensitive']),
spoiler_text : getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
visibility : getState().getIn(['compose', 'privacy']),
poll : getState().getIn(['compose', 'poll'], null),
}, {
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
@ -179,33 +178,33 @@ export function submitCompose(routerHistory) {
dispatch(submitComposeFail(error));
});
};
};
}
export function submitComposeRequest() {
return {
type: COMPOSE_SUBMIT_REQUEST,
};
};
}
export function submitComposeSuccess(status) {
return {
type: COMPOSE_SUBMIT_SUCCESS,
type : COMPOSE_SUBMIT_SUCCESS,
status: status,
};
};
}
export function submitComposeFail(error) {
return {
type: COMPOSE_SUBMIT_FAIL,
type : COMPOSE_SUBMIT_FAIL,
error: error,
};
};
}
export function uploadCompose(files) {
return function (dispatch, getState) {
const uploadLimit = 4;
const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
@ -231,15 +230,16 @@ export function uploadCompose(files) {
total += file.size - f.size;
return api(getState).post('/api/v1/media', data, {
onUploadProgress: function({ loaded }){
onUploadProgress: function ({ loaded }) {
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
}).catch(error => dispatch(uploadComposeFail(error)));
};
}
};
};
}
export function changeUploadCompose(id, params) {
return (dispatch, getState) => {
@ -251,68 +251,69 @@ export function changeUploadCompose(id, params) {
dispatch(changeUploadComposeFail(id, error));
});
};
};
}
export function changeUploadComposeRequest() {
return {
type: COMPOSE_UPLOAD_CHANGE_REQUEST,
type : COMPOSE_UPLOAD_CHANGE_REQUEST,
skipLoading: true,
};
};
}
export function changeUploadComposeSuccess(media) {
return {
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
media: media,
type : COMPOSE_UPLOAD_CHANGE_SUCCESS,
media : media,
skipLoading: true,
};
};
}
export function changeUploadComposeFail(error) {
return {
type: COMPOSE_UPLOAD_CHANGE_FAIL,
error: error,
type : COMPOSE_UPLOAD_CHANGE_FAIL,
error : error,
skipLoading: true,
};
};
}
export function uploadComposeRequest() {
return {
type: COMPOSE_UPLOAD_REQUEST,
type : COMPOSE_UPLOAD_REQUEST,
skipLoading: true,
};
};
}
export function uploadComposeProgress(loaded, total) {
return {
type: COMPOSE_UPLOAD_PROGRESS,
type : COMPOSE_UPLOAD_PROGRESS,
loaded: loaded,
total: total,
total : total,
};
};
}
export function uploadComposeSuccess(media, file) {
return {
type: COMPOSE_UPLOAD_SUCCESS,
media: media,
file: file,
type : COMPOSE_UPLOAD_SUCCESS,
media : media,
file : file,
skipLoading: true,
};
};
}
export function uploadComposeFail(error) {
return {
type: COMPOSE_UPLOAD_FAIL,
error: error,
type : COMPOSE_UPLOAD_FAIL,
error : error,
skipLoading: true,
};
};
}
export function undoUploadCompose(media_id) {
return {
type: COMPOSE_UPLOAD_UNDO,
type : COMPOSE_UPLOAD_UNDO,
media_id: media_id,
};
};
}
export function clearComposeSuggestions() {
if (cancelFetchComposeSuggestionsAccounts) {
@ -321,7 +322,7 @@ export function clearComposeSuggestions() {
return {
type: COMPOSE_SUGGESTIONS_CLEAR,
};
};
}
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
if (cancelFetchComposeSuggestionsAccounts) {
@ -334,9 +335,9 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
}),
params: {
q: token.slice(1),
q : token.slice(1),
resolve: false,
limit: 4,
limit : 4,
},
}).then(response => {
dispatch(importFetchedAccounts(response.data));
@ -366,10 +367,10 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
}),
params: {
type: 'hashtags',
q: token.slice(1),
resolve: false,
limit: 4,
type : 'hashtags',
q : token.slice(1),
resolve : false,
limit : 4,
exclude_unreviewed: true,
},
}).then(({ data }) => {
@ -395,7 +396,7 @@ export function fetchComposeSuggestions(token) {
break;
}
};
};
}
export function readyComposeSuggestionsEmojis(token, emojis) {
return {
@ -403,7 +404,7 @@ export function readyComposeSuggestionsEmojis(token, emojis) {
token,
emojis,
};
};
}
export function readyComposeSuggestionsAccounts(token, accounts) {
return {
@ -411,7 +412,7 @@ export function readyComposeSuggestionsAccounts(token, accounts) {
token,
accounts,
};
};
}
export const readyComposeSuggestionsTags = (token, tags) => ({
type: COMPOSE_SUGGESTIONS_READY,
@ -424,27 +425,27 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
let completion, startPosition;
if (suggestion.type === 'emoji') {
completion = suggestion.native || suggestion.colons;
completion = suggestion.native || suggestion.colons;
startPosition = position - 1;
dispatch(useEmoji(suggestion));
} else if (suggestion.type === 'hashtag') {
completion = `#${suggestion.name}`;
completion = `#${suggestion.name}`;
startPosition = position - 1;
} else if (suggestion.type === 'account') {
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
startPosition = position;
}
dispatch({
type: COMPOSE_SUGGESTION_SELECT,
type : COMPOSE_SUGGESTION_SELECT,
position: startPosition,
token,
completion,
path,
});
};
};
}
export function updateSuggestionTags(token) {
return {
@ -492,39 +493,39 @@ export function mountCompose() {
return {
type: COMPOSE_MOUNT,
};
};
}
export function unmountCompose() {
return {
type: COMPOSE_UNMOUNT,
};
};
}
export function changeComposeSensitivity() {
return {
type: COMPOSE_SENSITIVITY_CHANGE,
};
};
}
export function changeComposeSpoilerness() {
return {
type: COMPOSE_SPOILERNESS_CHANGE,
};
};
}
export function changeComposeSpoilerText(text) {
return {
type: COMPOSE_SPOILER_TEXT_CHANGE,
text,
};
};
}
export function changeComposeVisibility(value) {
return {
type: COMPOSE_VISIBILITY_CHANGE,
value,
};
};
}
export function insertEmojiCompose(position, emoji, needsSpace) {
return {
@ -533,33 +534,33 @@ export function insertEmojiCompose(position, emoji, needsSpace) {
emoji,
needsSpace,
};
};
}
export function changeComposing(value) {
return {
type: COMPOSE_COMPOSING_CHANGE,
value,
};
};
}
export function addPoll() {
return {
type: COMPOSE_POLL_ADD,
};
};
}
export function removePoll() {
return {
type: COMPOSE_POLL_REMOVE,
};
};
}
export function addPollOption(title) {
return {
type: COMPOSE_POLL_OPTION_ADD,
title,
};
};
}
export function changePollOption(index, title) {
return {
@ -567,14 +568,14 @@ export function changePollOption(index, title) {
index,
title,
};
};
}
export function removePollOption(index) {
return {
type: COMPOSE_POLL_OPTION_REMOVE,
index,
};
};
}
export function changePollSettings(expiresIn, isMultiple) {
return {
@ -582,4 +583,4 @@ export function changePollSettings(expiresIn, isMultiple) {
expiresIn,
isMultiple,
};
};
}

View File

@ -1,23 +1,19 @@
import api, { getLinks } from '../api';
import {
importFetchedAccounts,
importFetchedStatuses,
importFetchedStatus,
} from './importer';
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST';
export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
export const CONVERSATIONS_DELETE_REQUEST = 'CONVERSATIONS_DELETE_REQUEST';
export const CONVERSATIONS_DELETE_SUCCESS = 'CONVERSATIONS_DELETE_SUCCESS';
export const CONVERSATIONS_DELETE_FAIL = 'CONVERSATIONS_DELETE_FAIL';
export const CONVERSATIONS_DELETE_FAIL = 'CONVERSATIONS_DELETE_FAIL';
export const mountConversations = () => ({
type: CONVERSATIONS_MOUNT,
@ -30,7 +26,7 @@ export const unmountConversations = () => ({
export const markConversationRead = conversationId => (dispatch, getState) => {
dispatch({
type: CONVERSATIONS_READ,
id: conversationId,
id : conversationId,
});
api(getState).post(`/api/v1/conversations/${conversationId}/read`);

View File

@ -3,31 +3,31 @@ import openDB from '../storage/db';
import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
import { importAccount, importFetchedStatus, importFetchedStatuses, importStatus } from './importer';
import { ensureComposeIsVisible } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL';
export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL';
export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL';
export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL';
export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL';
export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL';
export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST';
export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS';
export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL';
export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL';
export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const STATUS_REVEAL = 'STATUS_REVEAL';
export const STATUS_HIDE = 'STATUS_HIDE';
export const STATUS_HIDE = 'STATUS_HIDE';
export const REDRAFT = 'REDRAFT';
@ -37,7 +37,7 @@ export function fetchStatusRequest(id, skipLoading) {
id,
skipLoading,
};
};
}
function getFromDB(dispatch, getState, accountIndex, index, id) {
return new Promise((resolve, reject) => {
@ -113,24 +113,24 @@ export function fetchStatus(id) {
dispatch(fetchStatusFail(id, error, skipLoading));
});
};
};
}
export function fetchStatusSuccess(skipLoading) {
return {
type: STATUS_FETCH_SUCCESS,
skipLoading,
};
};
}
export function fetchStatusFail(id, error, skipLoading) {
return {
type: STATUS_FETCH_FAIL,
type : STATUS_FETCH_FAIL,
id,
error,
skipLoading,
skipAlert: true,
};
};
}
export function redraft(status, raw_text) {
return {
@ -138,7 +138,7 @@ export function redraft(status, raw_text) {
status,
raw_text,
};
};
}
export function deleteStatus(id, routerHistory, withRedraft = false) {
return (dispatch, getState) => {
@ -163,29 +163,29 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
dispatch(deleteStatusFail(id, error));
});
};
};
}
export function deleteStatusRequest(id) {
return {
type: STATUS_DELETE_REQUEST,
id: id,
id : id,
};
};
}
export function deleteStatusSuccess(id) {
return {
type: STATUS_DELETE_SUCCESS,
id: id,
id : id,
};
};
}
export function deleteStatusFail(id, error) {
return {
type: STATUS_DELETE_FAIL,
id: id,
type : STATUS_DELETE_FAIL,
id : id,
error: error,
};
};
}
export function fetchContext(id) {
return (dispatch, getState) => {
@ -203,33 +203,33 @@ export function fetchContext(id) {
dispatch(fetchContextFail(id, error));
});
};
};
}
export function fetchContextRequest(id) {
return {
type: CONTEXT_FETCH_REQUEST,
id,
};
};
}
export function fetchContextSuccess(id, ancestors, descendants) {
return {
type: CONTEXT_FETCH_SUCCESS,
type : CONTEXT_FETCH_SUCCESS,
id,
ancestors,
descendants,
statuses: ancestors.concat(descendants),
};
};
}
export function fetchContextFail(id, error) {
return {
type: CONTEXT_FETCH_FAIL,
type : CONTEXT_FETCH_FAIL,
id,
error,
skipAlert: true,
};
};
}
export function muteStatus(id) {
return (dispatch, getState) => {
@ -241,21 +241,21 @@ export function muteStatus(id) {
dispatch(muteStatusFail(id, error));
});
};
};
}
export function muteStatusRequest(id) {
return {
type: STATUS_MUTE_REQUEST,
id,
};
};
}
export function muteStatusSuccess(id) {
return {
type: STATUS_MUTE_SUCCESS,
id,
};
};
}
export function muteStatusFail(id, error) {
return {
@ -263,7 +263,7 @@ export function muteStatusFail(id, error) {
id,
error,
};
};
}
export function unmuteStatus(id) {
return (dispatch, getState) => {
@ -275,21 +275,21 @@ export function unmuteStatus(id) {
dispatch(unmuteStatusFail(id, error));
});
};
};
}
export function unmuteStatusRequest(id) {
return {
type: STATUS_UNMUTE_REQUEST,
id,
};
};
}
export function unmuteStatusSuccess(id) {
return {
type: STATUS_UNMUTE_SUCCESS,
id,
};
};
}
export function unmuteStatusFail(id, error) {
return {
@ -297,7 +297,7 @@ export function unmuteStatusFail(id, error) {
id,
error,
};
};
}
export function hideStatus(ids) {
if (!Array.isArray(ids)) {
@ -308,7 +308,7 @@ export function hideStatus(ids) {
type: STATUS_HIDE,
ids,
};
};
}
export function revealStatus(ids) {
if (!Array.isArray(ids)) {
@ -319,4 +319,4 @@ export function revealStatus(ids) {
type: STATUS_REVEAL,
ids,
};
};
}

View File

@ -37,27 +37,27 @@ class Account extends ImmutablePureComponent {
handleFollow = () => {
this.props.onFollow(this.props.account);
}
};
handleBlock = () => {
this.props.onBlock(this.props.account);
}
};
handleMute = () => {
this.props.onMute(this.props.account);
}
};
handleMuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, true);
}
};
handleUnmuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, false);
}
};
handleAction = () => {
this.props.onActionClick(this.props.account);
}
};
render () {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle } = this.props;
@ -111,7 +111,10 @@ class Account extends ImmutablePureComponent {
<div className='account'>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<div className='account__avatar-wrapper'><Avatar
account={account}
size={55}
/></div >
<DisplayName account={account} />
</Permalink>

View File

@ -7,16 +7,16 @@ export default class Avatar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
size: PropTypes.number.isRequired,
style: PropTypes.object,
inline: PropTypes.bool,
size : PropTypes.number.isRequired,
style : PropTypes.object,
inline : PropTypes.bool,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
size: 20,
inline: false,
size : 20,
inline : false,
};
state = {
@ -26,14 +26,14 @@ export default class Avatar extends React.PureComponent {
handleMouseEnter = () => {
if (this.props.animate) return;
this.setState({ hovering: true });
}
};
handleMouseLeave = () => {
if (this.props.animate) return;
this.setState({ hovering: false });
}
};
render () {
render() {
const { account, size, animate, inline } = this.props;
const { hovering } = this.state;
@ -48,8 +48,8 @@ export default class Avatar extends React.PureComponent {
const style = {
...this.props.style,
width: `${size}px`,
height: `${size}px`,
width : `${size}px`,
height : `${size}px`,
backgroundSize: `${size}px ${size}px`,
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { version, source_url } from 'mastodon/initial_state';
import { source_url, version } from 'mastodon/initial_state';
export default class ErrorBoundary extends React.PureComponent {
@ -10,17 +10,17 @@ export default class ErrorBoundary extends React.PureComponent {
};
state = {
hasError: false,
stackTrace: undefined,
hasError : false,
stackTrace : undefined,
componentStack: undefined,
};
componentDidCatch (error, info) {
componentDidCatch(error, info) {
this.setState({
hasError: true,
stackTrace: error.stack,
hasError : true,
stackTrace : error.stack,
componentStack: info && info.componentStack,
copied: false,
copied : false,
});
}
@ -28,7 +28,7 @@ export default class ErrorBoundary extends React.PureComponent {
const { stackTrace } = this.state;
const textarea = document.createElement('textarea');
textarea.textContent = stackTrace;
textarea.textContent = stackTrace;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
@ -44,7 +44,7 @@ export default class ErrorBoundary extends React.PureComponent {
this.setState({ copied: true });
setTimeout(() => this.setState({ copied: false }), 700);
}
};
render() {
const { hasError, copied } = this.state;
@ -54,13 +54,32 @@ export default class ErrorBoundary extends React.PureComponent {
}
return (
<div className='error-boundary'>
<div>
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div>
</div>
<div className='error-boundary hero text-center padded content'>
<div >
<p className='error-boundary__error title content-heading'><FormattedMessage
id='error.unexpected_crash.explanation'
defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.'
/></p >
<p className='content'><FormattedMessage
id='error.unexpected_crash.next_steps'
defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.'
/></p >
<p className='error-boundary__footer'>Mastodon v{version} · <a
href={source_url}
rel='noopener noreferrer'
target='_blank'
><FormattedMessage
id='errors.unexpected_crash.report_issue'
defaultMessage='Report issue'
/></a > · <button
onClick={this.handleCopyStackTrace}
className={copied && 'copied'}
><FormattedMessage
id='errors.unexpected_crash.copy_stacktrace'
defaultMessage='Copy stacktrace to clipboard'
/></button ></p >
</div >
</div >
);
}

View File

@ -6,41 +6,41 @@ import Icon from 'mastodon/components/icon';
export default class IconButton extends React.PureComponent {
static propTypes = {
className: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
onClick: PropTypes.func,
className : PropTypes.string,
title : PropTypes.string.isRequired,
icon : PropTypes.string.isRequired,
onClick : PropTypes.func,
onMouseDown: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyPress: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
onKeyDown : PropTypes.func,
onKeyPress : PropTypes.func,
size : PropTypes.number,
active : PropTypes.bool,
pressed : PropTypes.bool,
expanded : PropTypes.bool,
style : PropTypes.object,
activeStyle: PropTypes.object,
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
disabled : PropTypes.bool,
inverted : PropTypes.bool,
animate : PropTypes.bool,
overlay : PropTypes.bool,
tabIndex : PropTypes.string,
};
static defaultProps = {
size: 18,
active: false,
size : 18,
active : false,
disabled: false,
animate: false,
overlay: false,
animate : false,
overlay : false,
tabIndex: '0',
};
state = {
activate: false,
activate : false,
deactivate: false,
}
};
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (!nextProps.animate) return;
if (this.props.active && !nextProps.active) {
@ -50,38 +50,38 @@ export default class IconButton extends React.PureComponent {
}
}
handleClick = (e) => {
handleClick = (e) => {
e.preventDefault();
if (!this.props.disabled) {
this.props.onClick(e);
}
}
};
handleKeyPress = (e) => {
if (this.props.onKeyPress && !this.props.disabled) {
this.props.onKeyPress(e);
}
}
};
handleMouseDown = (e) => {
if (!this.props.disabled && this.props.onMouseDown) {
this.props.onMouseDown(e);
}
}
};
handleKeyDown = (e) => {
if (!this.props.disabled && this.props.onKeyDown) {
this.props.onKeyDown(e);
}
}
};
render () {
render() {
const style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
// fontSize: `${this.props.size}px`,
// width: `${this.props.size * 1.28571429}px`,
// height: `${this.props.size * 1.28571429}px`,
// lineHeight: `${this.props.size}px`,
...this.props.style,
...(this.props.active ? this.props.activeStyle : {}),
};
@ -128,8 +128,12 @@ export default class IconButton extends React.PureComponent {
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
<Icon
id={icon}
fixedWidth
aria-hidden='true'
/>
</button >
);
}

View File

@ -16,7 +16,7 @@ import { Audio, MediaGallery, Video } from '../features/ui/util/async-components
import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { displayMedia } from '../initial_state';
import { displayMedia, isStaff } from '../initial_state';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
@ -82,11 +82,15 @@ class Status extends ImmutablePureComponent {
onMoveUp : PropTypes.func,
onMoveDown : PropTypes.func,
showThread : PropTypes.bool,
threadsCompile : PropTypes.bool,
getScrollPosition : PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth : PropTypes.func,
cachedMediaWidth : PropTypes.number,
};
static defaultProps = {
threadsCompile: true,
};
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
@ -199,24 +203,24 @@ class Status extends ImmutablePureComponent {
};
renderLoadingMediaGallery() {
return <div
return (<div
className='media-gallery'
style={{ height: '110px' }}
/>;
/>);
}
renderLoadingVideoPlayer() {
return <div
return (<div
className='video-player'
style={{ height: '110px' }}
/>;
/>);
}
renderLoadingAudioPlayer() {
return <div
return (<div
className='audio-player'
style={{ height: '110px' }}
/>;
/>);
}
handleOpenVideo = (media, startTime) => {
@ -497,20 +501,20 @@ class Status extends ImmutablePureComponent {
}
if (otherAccounts && otherAccounts.size > 0) {
statusAvatar = <AvatarComposite
statusAvatar = (<AvatarComposite
accounts={otherAccounts}
size={48}
/>;
size={55}
/>);
} else if (account === undefined || account === null) {
statusAvatar = <Avatar
statusAvatar = (<Avatar
account={status.get('account')}
size={48}
/>;
size={55}
/>);
} else {
statusAvatar = <AvatarOverlay
statusAvatar = (<AvatarOverlay
account={status.get('account')}
friend={account}
/>;
/>);
}
return (
@ -542,6 +546,12 @@ class Status extends ImmutablePureComponent {
role='presentation'
/>
<div className='status__info'>
{isStaff && (<div className='administrate-stuff pull-left'>
<i className='fa fa-gears' />
</div >
)}
<a
href={status.get('url')}
className='status__relative-time'
@ -595,10 +605,19 @@ class Status extends ImmutablePureComponent {
/>
</button >
)}
{/*<div className='well'>*/}
{/* {status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) &&*/}
{/* <p > oui je cause tout seul, c'est un thread</p >*/}
{/* }*/}
{/* {this.props.threadsCompile &&*/}
{/* <p > les threads sont en mode compilés</p >*/}
{/* }*/}
{/*</div >*/}
<StatusActionBar
status={status}
account={account} {...other} />
account={account} {...other}
/>
</div >
</div >
</HotKeys >

View File

@ -1,5 +1,5 @@
import 'intersection-observer';
import 'requestidlecallback';
import objectFitImages from 'object-fit-images';
import objectFitImages from 'object-fit-images';
objectFitImages();

View File

@ -1,10 +1,10 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import Button from 'mastodon/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
import { autoPlayGif, isStaff, me } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import Avatar from 'mastodon/components/avatar';
@ -70,7 +70,7 @@ class Header extends ImmutablePureComponent {
openEditProfile = () => {
window.open('/settings/profile', '_blank');
}
};
isStatusesPageActive = (match, location) => {
if (!match) {
@ -78,7 +78,7 @@ class Header extends ImmutablePureComponent {
}
return !location.pathname.match(/\/(followers|following)\/?$/);
}
};
_updateEmojis () {
const node = this.node;
@ -111,15 +111,15 @@ class Header extends ImmutablePureComponent {
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
}
};
handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static');
}
};
setRef = (c) => {
this.node = c;
}
};
render () {
const { account, intl, domain, identity_proofs } = this.props;
@ -263,7 +263,10 @@ class Header extends ImmutablePureComponent {
<div className='account__header__bar'>
<div className='account__header__tabs'>
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
<Avatar account={account} size={90} />
<Avatar
account={account}
size={120}
/>
</a>
<div className='spacer' />

View File

@ -21,12 +21,13 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
const path = withReplies ? `${accountId}:with_replies` : accountId;
return {
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
isAccount : !!state.getIn(['accounts', accountId]),
account : state.getIn(['accounts', accountId]),
statusIds : state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
isLoading : state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore : state.getIn(['timelines', `account:${path}`, 'hasMore']),
blockedBy : state.getIn(['relationships', accountId, 'blocked_by'], false),
};
};
@ -34,20 +35,20 @@ export default @connect(mapStateToProps)
class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
params : PropTypes.object.isRequired,
dispatch : PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
withReplies: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
multiColumn: PropTypes.bool,
statusIds : ImmutablePropTypes.list,
featuredStatusIds : ImmutablePropTypes.list,
isLoading : PropTypes.bool,
hasMore : PropTypes.bool,
withReplies : PropTypes.bool,
blockedBy : PropTypes.bool,
isAccount : PropTypes.bool,
multiColumn : PropTypes.bool,
};
componentWillMount () {
componentWillMount() {
const { params: { accountId }, withReplies } = this.props;
this.props.dispatch(fetchAccount(accountId));
@ -60,7 +61,7 @@ class AccountTimeline extends ImmutablePureComponent {
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
@ -74,33 +75,42 @@ class AccountTimeline extends ImmutablePureComponent {
}
handleLoadMore = maxId => {
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
}
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, {
maxId,
withReplies: this.props.withReplies,
}));
};
render () {
render() {
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn } = this.props;
if (!isAccount) {
return (
<Column>
<Column >
<ColumnBackButton multiColumn={multiColumn} />
<MissingIndicator />
</Column>
</Column >
);
}
if (!statusIds && isLoading) {
return (
<Column>
<Column >
<LoadingIndicator />
</Column>
</Column >
);
}
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />;
const emptyMessage = blockedBy ? <FormattedMessage
id='empty_column.account_unavailable'
defaultMessage='Profile unavailable'
/> : <FormattedMessage
id='empty_column.account_timeline'
defaultMessage='No toots here!'
/>;
return (
<Column>
<Column >
<ColumnBackButton multiColumn={multiColumn} />
<StatusList
@ -116,7 +126,7 @@ class AccountTimeline extends ImmutablePureComponent {
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>
</Column>
</Column >
);
}

View File

@ -46,6 +46,7 @@ class ComposeForm extends ImmutablePureComponent {
spoilerText : PropTypes.string,
focusDate : PropTypes.instanceOf(Date),
caretPosition : PropTypes.number,
maxTootCharsLimit : PropTypes.number,
preselectDate : PropTypes.instanceOf(Date),
isSubmitting : PropTypes.bool,
isChangingUpload : PropTypes.bool,
@ -64,7 +65,8 @@ class ComposeForm extends ImmutablePureComponent {
};
static defaultProps = {
showSearch: false,
showSearch : false,
maxTootCharsLimit: 7777,
};
handleChange = (e) => {
@ -88,7 +90,7 @@ class ComposeForm extends ImmutablePureComponent {
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > 7777 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > this.props.maxTootCharsLimit || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
return;
}
@ -181,23 +183,26 @@ class ComposeForm extends ImmutablePureComponent {
const { intl, onPaste, showSearch, anyMedia } = this.props;
const disabled = this.props.isSubmitting;
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 7777 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > this.props.maxTootCharsLimit || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText =
<span className='compose-form__publish-private'><Icon id='lock'/> {intl.formatMessage(messages.publish)}</span>;
<span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span >;
} else {
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
}
return (
<div className='compose-form'>
<WarningContainer/>
<WarningContainer />
<ReplyIndicatorContainer/>
<ReplyIndicatorContainer />
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}>
<div
className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}
ref={this.setRef}
>
<AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
@ -213,7 +218,7 @@ class ComposeForm extends ImmutablePureComponent {
id='cw-spoiler-input'
className='spoiler-input__input'
/>
</div>
</div >
<AutosuggestTextarea
ref={this.setAutosuggestTextarea}
@ -230,31 +235,36 @@ class ComposeForm extends ImmutablePureComponent {
onPaste={onPaste}
autoFocus={!showSearch && !isMobile(window.innerWidth)}
>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick}/>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
<div className='compose-form__modifiers'>
<UploadFormContainer/>
<PollFormContainer/>
</div>
</AutosuggestTextarea>
<UploadFormContainer />
<PollFormContainer />
</div >
</AutosuggestTextarea >
<div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'>
<UploadButtonContainer/>
<PollButtonContainer/>
<PrivacyDropdownContainer/>
<SpoilerButtonContainer/>
</div>
<div className='character-counter__wrapper'><CharacterCounter max={7777} text={text}/></div>
</div>
<UploadButtonContainer />
<PollButtonContainer />
<PrivacyDropdownContainer />
<SpoilerButtonContainer />
</div >
<div className='character-counter__wrapper'><CharacterCounter
max={this.props.maxTootCharsLimit}
text={text}
/></div >
</div >
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'><Button
text={publishText} onClick={this.handleSubmit}
disabled={disabledButton} block
/></div>
</div>
text={publishText}
onClick={this.handleSubmit}
disabled={disabledButton}
block
/></div >
</div >
</div>
</div >
);
}

View File

@ -26,7 +26,7 @@ export default class NavigationBar extends ImmutablePureComponent {
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span >
<Avatar
account={this.props.account}
size={48}
size={55}
/>
</Permalink >

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import Icon from 'mastodon/components/icon';
import AutosuggestInput from 'mastodon/components/autosuggest_input';
@ -56,19 +56,19 @@ class Option extends React.PureComponent {
if (e.key === 'Enter' || e.key === ' ') {
this.handleToggleMultiple(e);
}
}
};
onSuggestionsClearRequested = () => {
this.props.onClearSuggestions();
}
};
onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token);
}
};
onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
}
};
render () {
const { isPollMultiple, title, index, intl } = this.props;
@ -82,8 +82,8 @@ class Option extends React.PureComponent {
onKeyPress={this.handleCheckboxKeypress}
role='button'
tabIndex='0'
title={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
/>
<AutosuggestInput

View File

@ -2,62 +2,62 @@ import { connect } from 'react-redux';
import ComposeForm from '../components/compose_form';
import {
changeCompose,
submitCompose,
changeComposeSpoilerText,
clearComposeSuggestions,
fetchComposeSuggestions,
selectComposeSuggestion,
changeComposeSpoilerText,
insertEmojiCompose,
selectComposeSuggestion,
submitCompose,
uploadCompose,
} from '../../../actions/compose';
const mapStateToProps = state => ({
text: state.getIn(['compose', 'text']),
suggestions: state.getIn(['compose', 'suggestions']),
spoiler: state.getIn(['compose', 'spoiler']),
spoilerText: state.getIn(['compose', 'spoiler_text']),
privacy: state.getIn(['compose', 'privacy']),
focusDate: state.getIn(['compose', 'focusDate']),
caretPosition: state.getIn(['compose', 'caretPosition']),
preselectDate: state.getIn(['compose', 'preselectDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
text : state.getIn(['compose', 'text']),
suggestions : state.getIn(['compose', 'suggestions']),
spoiler : state.getIn(['compose', 'spoiler']),
spoilerText : state.getIn(['compose', 'spoiler_text']),
privacy : state.getIn(['compose', 'privacy']),
focusDate : state.getIn(['compose', 'focusDate']),
caretPosition : state.getIn(['compose', 'caretPosition']),
preselectDate : state.getIn(['compose', 'preselectDate']),
isSubmitting : state.getIn(['compose', 'is_submitting']),
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
isUploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
isUploading : state.getIn(['compose', 'is_uploading']),
showSearch : state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia : state.getIn(['compose', 'media_attachments']).size > 0,
});
const mapDispatchToProps = (dispatch) => ({
onChange (text) {
onChange(text) {
dispatch(changeCompose(text));
},
onSubmit (router) {
onSubmit(router) {
dispatch(submitCompose(router));
},
onClearSuggestions () {
onClearSuggestions() {
dispatch(clearComposeSuggestions());
},
onFetchSuggestions (token) {
onFetchSuggestions(token) {
dispatch(fetchComposeSuggestions(token));
},
onSuggestionSelected (position, token, suggestion, path) {
onSuggestionSelected(position, token, suggestion, path) {
dispatch(selectComposeSuggestion(position, token, suggestion, path));
},
onChangeSpoilerText (checked) {
onChangeSpoilerText(checked) {
dispatch(changeComposeSpoilerText(checked));
},
onPaste (files) {
onPaste(files) {
dispatch(uploadCompose(files));
},
onPickEmoji (position, data, needsSpace) {
onPickEmoji(position, data, needsSpace) {
dispatch(insertEmojiCompose(position, data, needsSpace));
},

View File

@ -150,7 +150,7 @@ class Conversation extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
const names = accounts.map(a => <Permalink
const names = accounts.map(a => (<Permalink
to={`/accounts/${a.get('id')}`}
href={a.get('url')}
key={a.get('id')}
@ -160,7 +160,7 @@ class Conversation extends ImmutablePureComponent {
className='display-name__html'
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
/></bdi >
</Permalink >).reduce((prev, cur) => [prev, ', ', cur]);
</Permalink >)).reduce((prev, cur) => [prev, ', ', cur]);
const handlers = {
reply : this.handleReply,
@ -179,7 +179,7 @@ class Conversation extends ImmutablePureComponent {
<div className='conversation__avatar'>
<AvatarComposite
accounts={accounts}
size={48}
size={55}
/>
</div >
@ -221,23 +221,24 @@ class Conversation extends ImmutablePureComponent {
)}
<div className='status__action-bar'>
<IconButton
className='status__action-bar-button'
title={intl.formatMessage(messages.reply)}
icon='reply'
onClick={this.handleReply}
/>
<div className='status__action-bar-dropdown'>
<DropdownMenuContainer
status={lastStatus}
items={menu}
icon='ellipsis-h'
size={18}
direction='right'
style={{ width: '15em' }}
size={18}
title={intl.formatMessage(messages.more)}
/>
</div >
<IconButton
className='status__action-bar-button conversation_reply'
title={intl.formatMessage(messages.reply)}
icon='reply'
size={40}
onClick={this.handleReply}
/>
</div >
</div >
</div >

View File

@ -9,10 +9,10 @@ import DisplayName from 'mastodon/components/display_name';
import Permalink from 'mastodon/components/permalink';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import IconButton from 'mastodon/components/icon_button';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
import { shortNumberFormat } from 'mastodon/utils/numbers';
import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'mastodon/actions/accounts';
import { blockAccount, followAccount, unblockAccount, unfollowAccount, unmuteAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes';
@ -113,27 +113,27 @@ class AccountCard extends ImmutablePureComponent {
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
}
};
handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static');
}
};
handleFollow = () => {
this.props.onFollow(this.props.account);
}
};
handleBlock = () => {
this.props.onBlock(this.props.account);
}
};
handleMute = () => {
this.props.onMute(this.props.account);
}
};
setRef = (c) => {
this.node = c;
}
};
render () {
const { account, intl } = this.props;
@ -165,7 +165,10 @@ class AccountCard extends ImmutablePureComponent {
<div className='directory__card__bar'>
<Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<Avatar account={account} size={48} />
<Avatar
account={account}
size={55}
/>
<DisplayName account={account} />
</Permalink>

View File

@ -31,7 +31,10 @@ class AccountAuthorize extends ImmutablePureComponent {
<div className='account-authorize__wrapper'>
<div className='account-authorize'>
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'>
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
<div className='account-authorize__avatar'><Avatar
account={account}
size={55}
/></div >
<DisplayName account={account} />
</Permalink>

View File

@ -5,11 +5,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import LoadingIndicator from '../../components/loading_indicator';
import {
fetchAccount,
fetchFollowing,
expandFollowing,
} from '../../actions/accounts';
import { expandFollowing, fetchAccount, fetchFollowing } from '../../actions/accounts';
import { FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
@ -19,34 +15,34 @@ import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator';
const mapStateToProps = (state, props) => ({
isAccount: !!state.getIn(['accounts', props.params.accountId]),
isAccount : !!state.getIn(['accounts', props.params.accountId]),
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
hasMore : !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
blockedBy : state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
});
export default @connect(mapStateToProps)
class Following extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
params : PropTypes.object.isRequired,
dispatch : PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
multiColumn: PropTypes.bool,
accountIds : ImmutablePropTypes.list,
hasMore : PropTypes.bool,
blockedBy : PropTypes.bool,
isAccount : PropTypes.bool,
multiColumn : PropTypes.bool,
};
componentWillMount () {
componentWillMount() {
if (!this.props.accountIds) {
this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(fetchFollowing(this.props.params.accountId));
}
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchFollowing(nextProps.params.accountId));
@ -57,29 +53,35 @@ class Following extends ImmutablePureComponent {
this.props.dispatch(expandFollowing(this.props.params.accountId));
}, 300, { leading: true });
render () {
render() {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props;
if (!isAccount) {
return (
<Column>
<Column >
<MissingIndicator />
</Column>
</Column >
);
}
if (!accountIds) {
return (
<Column>
<Column >
<LoadingIndicator />
</Column>
</Column >
);
}
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
const emptyMessage = blockedBy ? (<FormattedMessage
id='empty_column.account_unavailable'
defaultMessage='Profile unavailable'
/>) : (<FormattedMessage
id='account.follows.empty'
defaultMessage="This user doesn't follow anyone yet."
/>);
return (
<Column>
<Column >
<ColumnBackButton multiColumn={multiColumn} />
<ScrollableList
@ -87,16 +89,23 @@ class Following extends ImmutablePureComponent {
hasMore={hasMore}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
prepend={<HeaderContainer
accountId={this.props.params.accountId}
hideTabs
/>}
alwaysPrepend
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{blockedBy ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />
(<AccountContainer
key={id}
id={id}
withNote={false}
/>),
)}
</ScrollableList>
</Column>
</ScrollableList >
</Column >
);
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { HotKeys } from 'react-hotkeys';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
@ -13,10 +13,10 @@ import Permalink from 'mastodon/components/permalink';
const messages = defineMessages({
favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
follow: { id: 'notification.follow', defaultMessage: '{name} followed you' },
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
follow : { id: 'notification.follow', defaultMessage: '{name} followed you' },
ownPoll : { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
poll : { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
reblog : { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
});
const notificationForScreenReader = (intl, message, timestamp) => {
@ -35,31 +35,31 @@ class Notification extends ImmutablePureComponent {
};
static propTypes = {
notification: ImmutablePropTypes.map.isRequired,
hidden: PropTypes.bool,
onMoveUp: PropTypes.func.isRequired,
onMoveDown: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onToggleHidden: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
getScrollPosition: PropTypes.func,
notification : ImmutablePropTypes.map.isRequired,
hidden : PropTypes.bool,
onMoveUp : PropTypes.func.isRequired,
onMoveDown : PropTypes.func.isRequired,
onMention : PropTypes.func.isRequired,
onFavourite : PropTypes.func.isRequired,
onReblog : PropTypes.func.isRequired,
onToggleHidden : PropTypes.func.isRequired,
status : ImmutablePropTypes.map,
intl : PropTypes.object.isRequired,
getScrollPosition : PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
cacheMediaWidth : PropTypes.func,
cachedMediaWidth : PropTypes.number,
};
handleMoveUp = () => {
const { notification, onMoveUp } = this.props;
onMoveUp(notification.get('id'));
}
};
handleMoveDown = () => {
const { notification, onMoveDown } = this.props;
onMoveDown(notification.get('id'));
}
};
handleOpen = () => {
const { notification } = this.props;
@ -69,94 +69,131 @@ class Notification extends ImmutablePureComponent {
} else {
this.handleOpenProfile();
}
}
};
handleOpenProfile = () => {
const { notification } = this.props;
this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`);
}
};
handleMention = e => {
e.preventDefault();
const { notification, onMention } = this.props;
onMention(notification.get('account'), this.context.router.history);
}
};
handleHotkeyFavourite = () => {
const { status } = this.props;
if (status) this.props.onFavourite(status);
}
};
handleHotkeyBoost = e => {
const { status } = this.props;
if (status) this.props.onReblog(status, e);
}
};
handleHotkeyToggleHidden = () => {
const { status } = this.props;
if (status) this.props.onToggleHidden(status);
}
};
getHandlers () {
getHandlers() {
return {
reply: this.handleMention,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
mention: this.handleMention,
open: this.handleOpen,
openProfile: this.handleOpenProfile,
moveUp: this.handleMoveUp,
moveDown: this.handleMoveDown,
reply : this.handleMention,
favourite : this.handleHotkeyFavourite,
boost : this.handleHotkeyBoost,
mention : this.handleMention,
open : this.handleOpen,
openProfile : this.handleOpenProfile,
moveUp : this.handleMoveUp,
moveDown : this.handleMoveDown,
toggleHidden: this.handleHotkeyToggleHidden,
};
}
renderFollow (notification, account, link) {
renderFollow(notification, account, link) {
const { intl } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user-plus' fixedWidth />
</div>
<div
className='notification notification-follow focusable'
tabIndex='0'
aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}
>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
</span>
</div>
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
<span title={notification.get('created_at')}>
<span className='media'>
<span className='media-left'>
<AccountContainer
id={account.get('id')}
hidden={this.props.hidden}
/>
</span >
<span className='media-center'>
<FormattedMessage
id='notification.follow'
defaultMessage='{name} followed you'
values={{ name: link }}
/>
</span >
<span className='media-right'>
<Icon
id='user-plus'
fixedWidth
/>
</span >
</span >
</span >
</div >
</HotKeys >
);
}
renderFollowRequest (notification, account, link) {
renderFollowRequest(notification, account, link) {
const { intl } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-follow-request focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
<div
className='notification notification-follow-request focusable'
tabIndex='0'
aria-label={notificationForScreenReader(intl, intl.formatMessage({
id : 'notification.follow_request',
defaultMessage: '{name} has requested to follow you',
}, { name: account.get('acct') }), notification.get('created_at'))}
>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user' fixedWidth />
</div>
<Icon
id='user'
fixedWidth
/>
</div >
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.follow_request' defaultMessage='{name} has requested to follow you' values={{ name: link }} />
</span>
</div>
<FormattedMessage
id='notification.follow_request'
defaultMessage='{name} has requested to follow you'
values={{ name: link }}
/>
</span >
</div >
<FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
</div>
</HotKeys>
<FollowRequestContainer
id={account.get('id')}
withNote={false}
hidden={this.props.hidden}
/>
</div >
</HotKeys >
);
}
renderMention (notification) {
renderMention(notification) {
return (
<StatusContainer
id={notification.get('status')}
@ -173,21 +210,33 @@ class Notification extends ImmutablePureComponent {
);
}
renderFavourite (notification, link) {
renderFavourite(notification, link) {
const { intl } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div
className='notification notification-favourite focusable'
tabIndex='0'
aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}
>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='star' className='star-icon' fixedWidth />
</div>
<Icon
id='star'
className='star-icon'
fixedWidth
/>
</div >
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
</span>
</div>
<FormattedMessage
id='notification.favourite'
defaultMessage='{name} favourited your status'
values={{ name: link }}
/>
</span >
</div >
<StatusContainer
id={notification.get('status')}
@ -200,26 +249,37 @@ class Notification extends ImmutablePureComponent {
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</div >
</HotKeys >
);
}
renderReblog (notification, link) {
renderReblog(notification, link) {
const { intl } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div
className='notification notification-reblog focusable'
tabIndex='0'
aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}
>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='retweet' fixedWidth />
</div>
<Icon
id='retweet'
fixedWidth
/>
</div >
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
</span>
</div>
<FormattedMessage
id='notification.reblog'
defaultMessage='{name} boosted your status'
values={{ name: link }}
/>
</span >
</div >
<StatusContainer
id={notification.get('status')}
@ -232,32 +292,45 @@ class Notification extends ImmutablePureComponent {
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</div >
</HotKeys >
);
}
renderPoll (notification, account) {
renderPoll(notification, account) {
const { intl } = this.props;
const ownPoll = me === account.get('id');
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
const ownPoll = me === account.get('id');
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
<div
className='notification notification-poll focusable'
tabIndex='0'
aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}
>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='tasks' fixedWidth />
</div>
<Icon
id='tasks'
fixedWidth
/>
</div >
<span title={notification.get('created_at')}>
{ownPoll ? (
<FormattedMessage id='notification.own_poll' defaultMessage='Your poll has ended' />
<FormattedMessage
id='notification.own_poll'
defaultMessage='Your poll has ended'
/>
) : (
<FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' />
<FormattedMessage
id='notification.poll'
defaultMessage='A poll you have voted in has ended'
/>
)}
</span>
</div>
</span >
</div >
<StatusContainer
id={notification.get('status')}
@ -270,18 +343,24 @@ class Notification extends ImmutablePureComponent {
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</div >
</HotKeys >
);
}
render () {
render() {
const { notification } = this.props;
const account = notification.get('account');
const displayNameHtml = { __html: account.get('display_name_html') };
const link = <bdi><Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
const account = notification.get('account');
const displayNameHtml = { __html: account.get('display_name_html') };
const link = (<bdi ><Permalink
className='notification__display-name'
href={account.get('url')}
title={account.get('acct')}
to={`/accounts/${account.get('id')}`}
dangerouslySetInnerHTML={displayNameHtml}
/></bdi >);
switch(notification.get('type')) {
switch (notification.get('type')) {
case 'follow':
return this.renderFollow(notification, account, link);
case 'follow_request':

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchReblogs } from '../../actions/interactions';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
@ -25,15 +25,15 @@ export default @connect(mapStateToProps)
class Reblogs extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
params : PropTypes.object.isRequired,
dispatch : PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
accountIds : ImmutablePropTypes.list,
multiColumn : PropTypes.bool,
intl : PropTypes.object.isRequired,
};
componentWillMount () {
componentWillMount() {
if (!this.props.accountIds) {
this.props.dispatch(fetchReblogs(this.props.params.statusId));
}
@ -47,20 +47,23 @@ class Reblogs extends ImmutablePureComponent {
handleRefresh = () => {
this.props.dispatch(fetchReblogs(this.props.params.statusId));
}
};
render () {
render() {
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
<Column>
<Column >
<LoadingIndicator />
</Column>
</Column >
);
}
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.' />;
const emptyMessage = (<FormattedMessage
id='status.reblogs.empty'
defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.'
/>);
return (
<Column bindToDocument={!multiColumn}>
@ -68,7 +71,12 @@ class Reblogs extends ImmutablePureComponent {
showBackButton
multiColumn={multiColumn}
extraButton={(
<button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
<button
className='column-header__button'
title={intl.formatMessage(messages.refresh)}
aria-label={intl.formatMessage(messages.refresh)}
onClick={this.handleRefresh}
><Icon id='refresh' /></button >
)}
/>
@ -79,10 +87,14 @@ class Reblogs extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />
(<AccountContainer
key={id}
id={id}
withNote={false}
/>),
)}
</ScrollableList>
</Column>
</ScrollableList >
</Column >
);
}

View File

@ -45,15 +45,15 @@ export default class DetailedStatus extends ImmutablePureComponent {
}
e.stopPropagation();
}
};
handleOpenVideo = (media, startTime) => {
this.props.onOpenVideo(media, startTime);
}
};
handleExpandedToggle = () => {
this.props.onToggleHidden(this.props.status);
}
};
_measureHeight (heightJustChanged) {
if (this.props.measureHeight && this.node) {
@ -68,7 +68,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
setRef = c => {
this.node = c;
this._measureHeight();
}
};
componentDidUpdate (prevProps, prevState) {
this._measureHeight(prevState.height !== this.state.height);
@ -86,7 +86,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
}
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
}
};
render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
@ -211,8 +211,14 @@ export default class DetailedStatus extends ImmutablePureComponent {
<div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
<div className='detailed-status__display-avatar'><Avatar
account={status.get('account')}
size={55}
/></div >
<DisplayName
account={status.get('account')}
localDomain={this.props.domain}
/>
</a>
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />

View File

@ -551,8 +551,9 @@ class Status extends ImmutablePureComponent {
className={classNames('scrollable', { fullscreen })}
ref={this.setRef}
>
{/*<h2 className='debug'>ancestors:</h2 >*/}
{ancestors}
{/*<h2 className='debug'>common:</h2 >*/}
<HotKeys handlers={handlers}>
<div
className={classNames('focusable', 'detailed-status__wrapper')}
@ -591,7 +592,7 @@ class Status extends ImmutablePureComponent {
/>
</div >
</HotKeys >
{/*<h2 className='debug'>Descendants:</h2 >*/}
{descendants}
</div >
</ScrollContainer >

View File

@ -26,16 +26,29 @@ export default class ActionsModal extends ImmutablePureComponent {
return (
<li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
<div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
<div>{meta}</div>
</div>
</a>
</li>
<a
href={href}
target='_blank'
rel='noopener noreferrer'
onClick={this.props.onClick}
data-index={i}
className={classNames({ active })}
>
{icon && <IconButton
title={text}
icon={icon}
role='presentation'
tabIndex='-1'
inverted
/>}
<div >
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div >
<div >{meta}</div >
</div >
</a >
</li >
);
}
};
render () {
const status = this.props.status && (
@ -49,7 +62,10 @@ export default class ActionsModal extends ImmutablePureComponent {
<a href={this.props.status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
<Avatar account={this.props.status.get('account')} size={48} />
<Avatar
account={this.props.status.get('account')}
size={55}
/>
</div>
<DisplayName account={this.props.status.get('account')} />

View File

@ -1,7 +1,7 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import Button from '../../../components/button';
import StatusContent from '../../../components/status_content';
import Avatar from '../../../components/avatar';
@ -37,7 +37,7 @@ class BoostModal extends ImmutablePureComponent {
handleReblog = () => {
this.props.onReblog(this.props.status);
this.props.onClose();
}
};
handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
@ -45,11 +45,11 @@ class BoostModal extends ImmutablePureComponent {
this.props.onClose();
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
}
}
};
setRef = (c) => {
this.button = c;
}
};
render () {
const { status, intl } = this.props;
@ -66,7 +66,10 @@ class BoostModal extends ImmutablePureComponent {
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
<Avatar account={status.get('account')} size={48} />
<Avatar
account={status.get('account')}
size={55}
/>
</div>
<DisplayName account={status.get('account')} />

View File

@ -31,6 +31,8 @@ import NavigationPanel from './navigation_panel';
import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
import LinkFooter from './link_footer';
import InstantMessaging from './messaging/instantMessaging';
const componentMap = {
'COMPOSE' : Compose,
@ -157,10 +159,10 @@ class ColumnsArea extends ImmutablePureComponent {
const view = (index === columnIndex) ?
React.cloneElement(this.props.children) :
<ColumnLoading
(<ColumnLoading
title={title}
icon={icon}
/>;
/>);
return (
<div
@ -187,12 +189,12 @@ class ColumnsArea extends ImmutablePureComponent {
const columnIndex = getIndex(this.context.router.history.location.pathname);
if (singleColumn) {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : (<Link
key='floating-action-button'
to='/statuses/new'
className='floating-action-button'
aria-label={intl.formatMessage(messages.publish)}
><Icon id='pencil' /></Link >;
><Icon id='pencil' /></Link >);
const content = columnIndex !== -1 ? (
<ReactSwipeableViews
@ -217,10 +219,13 @@ class ColumnsArea extends ImmutablePureComponent {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
<ComposePanel />
</div >
</div >
<div className='columns-area__panels__main'>
@ -233,8 +238,12 @@ class ColumnsArea extends ImmutablePureComponent {
<NavigationPanel />
</div >
</div >
<LinkFooter withHotkeys />
{floatingActionButton}
<div className='hidden_nope'>
<InstantMessaging />
</div >
</div >
);
}
@ -255,17 +264,20 @@ class ColumnsArea extends ImmutablePureComponent {
loading={this.renderLoading(column.get('id'))}
error={this.renderError}
>
{SpecificComponent => <SpecificComponent
{SpecificComponent => (<SpecificComponent
columnId={column.get('uuid')}
params={params}
multiColumn {...other} />}
multiColumn {...other}
/>)}
</BundleContainer >
);
})}
{React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
</div >
);
)
;
}
}

View File

@ -2,15 +2,13 @@ import React from 'react';
import SearchContainer from 'mastodon/features/compose/containers/search_container';
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
import LinkFooter from './link_footer';
const ComposePanel = () => (
<div className='compose-panel'>
<SearchContainer openInRoute />
<NavigationContainer />
<ComposeFormContainer singleColumn />
<LinkFooter withHotkeys />
</div>
</div >
);
export default ComposePanel;

View File

@ -2,10 +2,11 @@ import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { Link, NavLink } from 'react-router-dom';
import { invitesEnabled, repository, source_url, version } from 'mastodon/initial_state';
import { logOut } from 'mastodon/utils/log_out';
import { openModal } from 'mastodon/actions/modal';
import { isStaff } from '../../../initial_state';
const messages = defineMessages({
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
@ -22,14 +23,26 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
});
// const displaythemetoggler = true;
export default @injectIntl
@connect(null, mapDispatchToProps)
class LinkFooter extends React.PureComponent {
static propTypes = {
withHotkeys: PropTypes.bool,
onLogout : PropTypes.func.isRequired,
intl : PropTypes.object.isRequired,
enableChristmasSnow : PropTypes.bool,
minimumWeekToShowSnow: PropTypes.number,
snowActive : PropTypes.bool,
withHotkeys : PropTypes.bool,
snow : PropTypes.func,
themeIsDark : PropTypes.bool,
theme : PropTypes.string,
onLogout : PropTypes.func.isRequired,
intl : PropTypes.object.isRequired,
};
static defaultProps = {
enableChristmasSnow : true,
themeIsDark : true,
minimumWeekToShowSnow: 48,
};
handleLogoutClick = e => {
@ -41,35 +54,177 @@ class LinkFooter extends React.PureComponent {
return false;
};
constructor(props) {
super(props);
Date.prototype.getWeek = function () {
var onejan = new Date(this.getFullYear(), 0, 1);
return Math.ceil((((this - onejan) / 86400000) + onejan.getDay() + 1) / 7);
};
var weekNumber = (new Date()).getWeek();
// display snow during the last two weeks of the year
const shouldWeDisplaySnow = (weekNumber > props.minimumWeekToShowSnow) && props.enableChristmasSnow;
this.state = {
enableChristmasSnow: shouldWeDisplaySnow,
theme : props.theme,
};
// make snow effect
if (shouldWeDisplaySnow) {
import('../../../utils/snowstorm-min')
.then((snowstorm) => {
Window.snowstorm = snowstorm.default;
this.state.snow = Window.snowstorm;
// snowstorm.start();
this.state.snowActive = true;
})
.catch((err) => console.error(err));
}
}
toggleSnow = () => {
if (this.state.snow) {
if (this.state.snowActive) {
this.state.snow.stop();
this.state.enableChristmasSnow = false;
} else {
this.state.snow.start();
this.state.enableChristmasSnow = true;
}
}
};
changeTheme(newTheme) {
console.log('change theme en ', newTheme);
}
render() {
const HashTagNavlinks = ['Mastoart', 'OpenStreetMaps', 'Ironèmes', 'vélo'];
const { withHotkeys } = this.props;
var snowClasses = this.props.enableChristmasSnow ? 'snow-button active' : 'snow-button ';
const navToTags = HashTagNavlinks.map(element => {
return (
<li
className='tag-element btn-small btn'
key={element}
>
<NavLink
exact
activeClassName='active'
to={'/timelines/tag/' + element}
title='Mastoart'
>
#{element}
</NavLink >
</li >
);
});
return (
<div className='getting-started__footer'>
<ul >
<li >
<a href='https://liberapay.com/cipherbliss'>Supportez Cipherbliss</a >
</li >
<li >
<div className='links-started__footer desktop-only'>
<div className='extras'>
{/*<button className='mod-theme btn btn-block btn-small btn-primary pull-left'>*/}
{/* {this.themeIsDark ? (*/}
{/* <span*/}
{/* onClick={this.setState('theme', 'light')}*/}
{/* title='set light'*/}
{/* >*/}
{/* <i className='fa fa-pencil-o' /> to light*/}
{/* </span >*/}
{/* ) : (*/}
{/* <span*/}
{/* onClick={*/}
{/* this.changeTheme*/}
{/* }*/}
{/* title='set dark'*/}
{/* >*/}
{/* <i className='fa fa-pencil' /> to dark*/}
{/* </span >*/}
{/* )}*/}
{/*</button >*/}
{this.state.enableChristmasSnow && (
<div
onClick={this.toggleSnow}
className='christmas-snow'
>
<div className={snowClasses}>
<i
className='icon fa fa-snowflake-o'
aria-hidden='true'
/>
</div >
<div > Joyeuses fêtes!</div >
</div >
)}
{isStaff && (
<span className='staff-actions'>
<NavLink
exact
activeClassName='active'
to={'/tk-example/'}
title='tk example link'
>
example link
</NavLink >
<a
className='btn-warning'
href='/admin/tags?pending_review=1'
>
<i className='fa fa-fire' />
Trending hashtags
</a >
<a
className='btn-warning'
href='/admin/accounts'
>
<i className='fa fa-users' />
Comptes
</a >
</span >
)}
<br />
<div className='external-utilities'>
<a href='https://mastodon.cipherbliss.com/@tykayn'>
<i className='fa fa-paper-plane' />
contactez nous</a >
</li >
<li >
contactez nous
</a >
<a href='https://liberapay.com/cipherbliss'><i className='fa fa-coffee' /> Supportez
Cipherbliss</a >
<a href='/admin/tags?pending_review=1'>
<i className='fa fa-fire' />
Trending hashtags</a >
<a href='https://peertube.cipherbliss.com'> <i className='fa fa-play ' /> Videos</a >
<a href='https://framadate.org/'> <i className='fa fa-calendar' /> FramaDate</a >
<a href='https://framapad.org/'> <i className='fa fa-file-text' /> Pad</a >
<a href='https://framagit.org/tykayn/mastodon'> <i className='fa fa-gitlab' /> Source</a >
<hr />
</li >
{invitesEnabled && <li ><a
href='/invites'
target='_blank'
><FormattedMessage
id='getting_started.invite'
defaultMessage='Invite people'
/> ·</a >
</li >}
<div className='suggested-tags'>
<ul >
{navToTags}
</ul >
</div >
</div >
</div >
<ul >
{invitesEnabled && (
<li >
<a
href='/invites'
target='_blank'
>
<FormattedMessage
id='getting_started.invite'
defaultMessage='Invite people'
/> ·</a >
</li >
)}
{withHotkeys && <li ><Link to='/keyboard-shortcuts'>
<FormattedMessage
id='navigation_bar.keyboard_shortcuts'

View File

@ -1,147 +0,0 @@
import ImmutablePureComponent from 'react-immutable-pure-component';
export default class Messaging extends ImmutablePureComponent {
// static propTypes = {
// following : ImmutablePropTypes.list,
// conversations: ImmutablePropTypes.list,
// newMessage : ImmutablePropTypes.string,
// };
// openConversationWith(account) {
// let conversationFound = account;
// if conversation exist, focus on it
// if (conversationFound) {
//
// } else {
//
// }
// else, create conversation and focus on it
// };
submitCompose() {
};
constructor() {
super();
// this.props.newMessage = 'meh';
// this.props.conversations = [
// {
// withAccount: '@machin',
// messages : [],
// opened : true,
// },
// {
// withAccount: '@chuck',
// messages : [],
// opened : false,
// },
// ];
// this.props.following = [
// { username: 'wulfila', handle: '@wulfila' },
// { username: 'machin', handle: '@machin' },
// { username: 'chuck norris', handle: '@chuck' },
// ];
}
render() {
// const contactlist = null;
return (
<div>
messagerie todo
</div>
);
// const contactlist = this.props.following.foreEach(elem => (
// <li className='user-item'>
// <div
// className='username'
// onClick={this.openConversationWith(elem.username)}
// >
// Machin
// </div >
// <div className='last-active'>3 min</div >
// </li >
// ));
// return (
// <div className='messaging-container'>
// <div className='messaging-box'>
// <div className='title'>
// <i
// role='img'
// className='fa fa-envelope column-header__icon fa-fw'
// />
// Messaging box
// </div >
// <div className='user-list column-header'>
// <h2 className='title'>User list</h2 >
// <ul >
// {contactlist}
// </ul >
// </div >
//
// </div >
// <div className='conversations_list'>
// <ul >
// <li className='conversations_item has-new-message'>
// <div className='title'>
// <i
// role='img'
// className='fa fa-envelope column-header__icon fa-fw'
// />
// Un Gens
// <span className='new-message-counter'>
// (3)</span >
// <button className='btn-small'>
// <i
// role='img'
// className='fa fa-caret-down column-header__icon fa-fw'
// />
// </button >
// </div >
// <div className='conversation_stream'>
// <div className='message theirs'>
// <p >oh hello there! 😋 </p >
// <div className='arrow-down' />
// </div >
// <div className='message mine'>
// <p >General Emoji</p >
// <div className='arrow-down' />
// </div >
// <div className='message theirs'>
// <p >we just achieved comedy</p >
// <div className='arrow-down' />
// </div >
// </div >
// <div className='conversation_input'>
// <form
// action='#'
// // onSubmit={this.submitCompose()}
// >
// {/*value={this.newMessage.toString()}*/}
// <textarea
// name='messager'
// id=''
// cols='30'
// rows='10'
// className='messager-textarea'
// placeholder='allez dis nous tout'
//
// />
// <input
// type='submit'
// name='submit'
// value='Send'
// />
// </form >
// </div >
// </li >
// </ul >
// </div >
// </div >
// );
}
};

View File

@ -0,0 +1,65 @@
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import React from 'react';
import Permalink from '../../../../components/permalink';
export default class Contact extends ImmutablePureComponent {
static propTypes = {
account: PropTypes.object,
};
static defaultProps = {};
constructor(props) {
super(props);
this.state = {
account: this.props.account,
};
}
openConversationWithAccount = (accountId) => {
dispatchEvent({ type: 'openConversation', target: accountId });
};
render() {
const account = this.props.account;
return (
<div
className='contact media'
onClick={this.openConversationWithAccount}
>
<div className='name media-left'>
<Permalink
key={account.id}
className='account__display-name'
title={account.acct}
href={account.url}
to={`/accounts/${account.id}`}
>
<div className='avatar image is-32x32'>
<img
className='is-rounded'
src={this.props.account.avatar}
alt='avatar'
/>
</div >
</Permalink >
</div >
<div className='media-content'>
<div className='username'>
{this.props.account.username}
</div >
</div >
<div className='media-right'>
<div className='last-seen'>
<i className='fa fa-clock-o' />
</div >
</div >
</div >
);
}
}

View File

@ -0,0 +1,142 @@
import React from 'react';
import PropTypes from 'prop-types';
import accounts, { me } from '../../../../initial_state';
import api from '../../../../api';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { mockContactList } from './mocks/mockContactList';
import Contact from './Contact';
import classNames from 'classnames';
export default class ContactsList extends ImmutablePureComponent {
static propTypes = {
myAccount : PropTypes.array,
showList : PropTypes.bool,
contactList : PropTypes.array,
conversationList: PropTypes.array,
following_count : PropTypes.number,
};
static defaultProps = {
showList : true,
myAccount : null,
userID : me,
following_count : 0,
contactList : mockContactList,
conversationList: mockContactList,
};
constructor(props) {
super(props);
this.state = {
following_count : accounts.accounts[me].following_count,
showList : props.showList,
myAccount : accounts.accounts[me],
contactList : mockContactList,
conversationList: mockContactList,
};
// this.fetchContacts(1);
}
submitCompose() {
console.log('submit message');
}
toggleList = () => {
this.setState((state) => {
return {
showList: !state.showList,
};
});
};
/**
* find followed accounts
* @param AccountID
* @returns {Promise<AxiosResponse<T> | void>}
*/
fetchContacts = (AccountID = me) => {
return api(this.getState()).get('/api/v1/accounts/' + AccountID + '/following').then(resp => {
console.log('resp', resp);
}).catch(err => console.error('err', err));
};
render() {
let renderedList = (
<span >no contacts</span >
);
if (this.props.contactList) {
renderedList = this.props.contactList.map(account => {
return (
<li
className='contact-item'
key={account.id}
>
<Contact account={account} />
</li >
);
});
}
const showListClass = (this.state.showList ? 'active' : 'inactive');
const classList = 'btn btn-primary toggle-list ' + showListClass;
return (
<div className='messaging-container'>
<link
rel='stylesheet'
type='text/css'
media='screen'
href='https://cdn.conversejs.org/6.0.0/dist/converse.min.css'
/>
{/*<script*/}
{/* src='https://cdn.conversejs.org/6.0.0/dist/converse.min.js'*/}
{/* charSet='utf-8'*/}
{/*></script >*/}
{/*<script >*/}
{/* converse.initialize({*/}
{/* bosh_service_url : 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes*/}
{/* show_controlbox_by_default: true*/}
{/*});*/}
{/*</script >*/}
<div
className={classNames('messaging-box ', {
'active' : this.state.showList,
'inactive': !this.state.showList,
})}
>
<div className='card-title '>
<i
role='img'
className='fa fa-comment column-header__icon fa-fw'
/>
{this.state.following_count} contacts
<button
className={classList}
onClick={this.toggleList}
>
{this.state.showList && (
<i className='fa fa-caret-up' />
)}
{!this.state.showList && (
<i className='fa fa-caret-left' />
)}
</button >
</div >
<div className='user-list'>
{this.state.showList && (
<div className='contact-list-container'>
<ul className='contact-list'>
{renderedList}
</ul >
</div >
)}
</div >
</div >
</div >
);
};
}

View File

@ -0,0 +1,116 @@
import React from 'react';
import PropTypes from 'prop-types';
import ConversationStream from './conversation-stream';
/**
* a conversation between the current logged in user and one recipient
*/
export default class ConversationItem extends React.PureComponent {
static propTypes = {
messages : PropTypes.array, // our and their message sorted chronologically
recipient : PropTypes.any, // account of the person we talk to, not current logged in account
newMessages: PropTypes.number,
displayed : PropTypes.bool,
};
static defaultProps = {
newMessages: 0,
displayed : true,
};
following = [];
constructor(props) {
super(props);
console.log('props', props);
this.state = {
composeMessage: '',
displayed : this.props.displayed,
newMessages : this.props.newMessages,
isFocused : false,
};
}
submitCompose = (e) => {
e.preventDefault();
console.log('submit');
};
toggleVisibility = () => {
this.setState({ 'displayed': !this.state.displayed });
};
toggleFocused = () => {
this.setState({ 'isFocused': !this.state.isFocused });
};
handleChange = (e) => {
e.preventDefault();
console.log('e', e);
};
render() {
const hasNewClass = this.state.newMessages ? 'has-new-message' : 'nothing-new';
const isVisible = this.state.displayed ? 'displayed' : 'hidden';
const isFocused = this.state.isFocused ? 'isFocused' : 'not-focused';
const list = (
<li className={'conversation-item ' + hasNewClass + ' ' + isVisible + ' ' + isFocused}>
<div className='top-title'>
<i
role='img'
className='fa fa-comment-o column-header__icon fa-fw'
/>
<span className='username'>
{this.props.recipient.username}
</span >
{/*<Contact account={this.props.recipient} />*/}
{this.props.newMessages > 0 && (
<span className='new-message-counter'>
({this.props.newMessages})
</span >
)}
<button
className='btn btn-small'
onClick={this.toggleVisibility}
>
<i
role='img'
className='fa fa-caret-down column-header__icon fa-fw'
/>
</button >
</div >
<ConversationStream messages={this.props.messages} />
<div className='conversation_input'>
<form
action='#'
onSubmit={this.submitCompose}
>
<textarea
name='messager'
id=''
cols='15'
rows='3'
className='messager-textarea'
placeholder='allez dis nous tout'
onFocusCapture={this.toggleFocused}
onBlurCapture={this.toggleFocused}
onChange={this.handleChange}
/>
<input
type='submit'
name='submit'
value='Send'
/>
</form >
</div >
</li >
);
return (
<ul >
{list}
</ul >
);
}
}

View File

@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class ConversationStream extends React.PureComponent {
static propTypes = {
messages: PropTypes.array,
};
render() {
let messagesLists = (
<div className='no_messages'>
no messages
</div >
);
if (this.props.messages) {
messagesLists = this.props.messages.map(message => {
return (
<li
className={'message ' + message.who}
key={message.id}
>
<p >{message.text}</p >
<div className='arrow-down' />
</li >
);
});
}
return (
<div className='conversation-stream'>
<div className='messages'>
{messagesLists}
</div >
</div >
);
}
}

View File

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import ConversationItem from './conversation-item';
import { mockRecipient, mockRecipient2 } from './mocks/mockConversation';
export default class ConversationStack extends React.Component {
static propTypes = {
conversations: PropTypes.array,
};
static defaultProps = {
conversations: [mockRecipient, mockRecipient2],
};
openConversationWith(account) {
console.log('openConversationWith name', account.username);
}
render() {
let list = this.props.conversations.map(recipient => {
console.log('recipient', recipient);
recipient = recipient[0];
return (
<li
className='conversation-item-wrapper'
key={'wrapper' + recipient.id}
>
<ConversationItem
recipient={recipient}
messages={recipient.messages}
key={'ConversationItem_' + recipient.id}
onClick={this.openConversationWith}
/>
</li >
);
},
)
;
return (
<ul className='stack conversations_list'>
{list}
</ul >
);
};
}

View File

@ -0,0 +1,60 @@
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ContactsList from './contacts-list';
import ConversationStack from './conversationStack';
const mapStateToProps = (state, props) => ({
isAccount : !!state.getIn(['accounts', props.params.accountId]),
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
hasMore : !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
blockedBy : state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
});
// @connect(mapStateToProps)
/**
* main component for IM, gathers contact list and list of conversations
*/
export default class InstantMessaging extends ImmutablePureComponent {
static propTypes = {
// params : PropTypes.object.isRequired,
dispatch : PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds : ImmutablePropTypes.list,
hasMore : PropTypes.bool,
blockedBy : PropTypes.bool,
isAccount : PropTypes.bool,
multiColumn : PropTypes.bool,
};
// static defaultProps = {
// threadsCompile: true,
// };
// openConversationWith(account) {
// let conversationFound = account;
// if conversation exist, focus on it
// if (conversationFound) {
//
// } else {
//
// }
// else, create conversation and focus on it
// };
// submitCompose() {
//
// };
render() {
return (
<div className='main-instant-messaging'>
<ContactsList />
<ConversationStack />
</div >
);
}
};

View File

@ -0,0 +1,69 @@
export const mockContactList = [{
'key' : '2',
'id' : '2',
'username' : 'demoguy',
'acct' : 'demoguy',
'display_name' : '',
'locked' : false,
'bot' : false,
'discoverable' : null,
'group' : false,
'created_at' : '2019-12-18T11:02:26.494Z',
'note' : '<p></p>',
'url' : 'http://localhost:3000/@demoguy',
'avatar' : 'http://localhost:3000/avatars/original/missing.png',
'avatar_static' : 'http://localhost:3000/avatars/original/missing.png',
'header' : 'http://localhost:3000/headers/original/missing.png',
'header_static' : 'http://localhost:3000/headers/original/missing.png',
'followers_count' : 1,
'following_count' : 1,
'statuses_count' : 8,
'last_status_at' : '2019-12-23T15:20:31.575Z',
'emojis' : [],
'fields' : [],
'display_name_html': 'demoguy',
'note_emojified' : '<p></p>',
}, {
'key' : '1',
'id' : '1',
'username' : 'admin',
'acct' : 'admin',
'display_name' : '',
'locked' : true,
'bot' : false,
'discoverable' : true,
'group' : false,
'created_at' : '2019-12-10T13:19:44.106Z',
'note' : '<p></p>',
'url' : 'http://localhost:3000/@admin',
'avatar' : 'http://localhost:3000/system/accounts/avatars/000/000/001/original/a1497a4af5fd8616.png?1576254102',
'avatar_static' : 'http://localhost:3000/system/accounts/avatars/000/000/001/original/a1497a4af5fd8616.png?1576254102',
'header' : 'http://localhost:3000/system/accounts/headers/000/000/001/original/f13ebf964b09ed26.png?1576254102',
'header_static' : 'http://localhost:3000/system/accounts/headers/000/000/001/original/f13ebf964b09ed26.png?1576254102',
'followers_count' : 4,
'following_count' : 3,
'statuses_count' : 31,
'last_status_at' : '2020-01-04T15:13:32.824Z',
'emojis' : [],
'fields' : [
{
'name' : 'sssss',
'value' : 'muuuuu',
'verified_at' : null,
'name_emojified' : 'sssss',
'value_emojified': 'muuuuu',
'value_plain' : 'muuuuu',
},
],
'display_name_html': 'admin',
'note_emojified' : '<p></p>',
}];
export const mockContactListShort = [{
'key' : '2',
'id' : '2',
'username': 'demoguy',
}, {
'key' : '1',
'id' : '1',
'username': 'admin',
}];

View File

@ -0,0 +1,33 @@
export const mockMessages = [
{ id: 0, text: 'oh hello there! 😋 ', who: 'theirs' },
{ id: 1, text: 'General Emoji', who: 'ours' },
{ id: 2, text: 'we just achieved comedy', who: 'theirs' },
]
;
export const mockMessages2 = [
{ id: 0, text: 'oh oh oh ', who: 'theirs' },
{ id: 1, text: 'General Emoji', who: 'ours' },
{ id: 2, text: 'DANGER!!', who: 'theirs' },
{ id: 3, text: 'JUST KIDDING WILL ROBINSON.', who: 'theirs' },
]
;
export const mockRecipient = [
{
id : 3,
username : 'chuck norris',
newMessages: 5,
messages : mockMessages,
},
]
;
export const mockRecipient2 = [
{
id : 4,
username : 'the Bo Botte',
newMessages: 0,
messages : mockMessages2,
},
]
;

View File

@ -8,24 +8,53 @@ import FollowRequestsNavLink from './follow_requests_nav_link';
import ListPanel from './list_panel';
import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
const showMessaging = true;
const NavigationPanel = () => (
<div className='navigation-panel'>
<div className='small-texts timelines'>
<NavLink
className='column-link column-link--transparent'
to='/timelines/home'
data-preview-title-id='column.home'
data-preview-icon='home'
><Icon
className='column-link__icon'
id='home'
fixedWidth
/><FormattedMessage
id='tabs_bar.home'
defaultMessage='Home'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/timelines/public/local'
data-preview-title-id='column.community'
data-preview-icon='users'
><Icon
className='column-link__icon'
id='users'
fixedWidth
/><FormattedMessage
id='tabs_bar.local_timeline'
defaultMessage='Local'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/timelines/home'
data-preview-title-id='column.home'
data-preview-icon='home'
><Icon
className='column-link__icon'
id='home'
fixedWidth
/><FormattedMessage
id='tabs_bar.home'
defaultMessage='Home'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
exact
to='/timelines/public'
data-preview-title-id='column.public'
data-preview-icon='globe'
><Icon
className='column-link__icon'
id='globe'
fixedWidth
/><FormattedMessage
id='tabs_bar.federated_timeline'
defaultMessage='Federated'
/></NavLink >
</div >
<div className='spacer'></div >
<NavLink
className='column-link column-link--transparent'
to='/notifications'
@ -37,34 +66,6 @@ const NavigationPanel = () => (
id='tabs_bar.notifications'
defaultMessage='Notifications'
/></NavLink >
<FollowRequestsNavLink />
<NavLink
className='column-link column-link--transparent'
to='/timelines/public/local'
data-preview-title-id='column.community'
data-preview-icon='users'
><Icon
className='column-link__icon'
id='users'
fixedWidth
/><FormattedMessage
id='tabs_bar.local_timeline'
defaultMessage='Local'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
exact
to='/timelines/public'
data-preview-title-id='column.public'
data-preview-icon='globe'
><Icon
className='column-link__icon'
id='globe'
fixedWidth
/><FormattedMessage
id='tabs_bar.federated_timeline'
defaultMessage='Federated'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/timelines/direct'
@ -76,6 +77,10 @@ const NavigationPanel = () => (
id='navigation_bar.direct'
defaultMessage='Direct messages'
/></NavLink >
<FollowRequestsNavLink />
<div className='spacer'></div >
<NavLink
className='column-link column-link--transparent'
to='/favourites'
@ -151,8 +156,6 @@ const NavigationPanel = () => (
{showTrends && <div className='flex-spacer' />}
{showTrends && <TrendsContainer />}
{/*{showMessaging && <Messaging />}*/}
{/*<Messaging />*/}
</div >
);

View File

@ -8,19 +8,81 @@ import Icon from 'mastodon/components/icon';
import NotificationsCounterIcon from './notifications_counter_icon';
export const links = [
<NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
<NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
<NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
<NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
<NavLink
className='tabs-bar__link'
to='/timelines/home'
data-preview-title-id='column.home'
data-preview-icon='home'
><Icon
id='home'
fixedWidth
/><FormattedMessage
id='tabs_bar.home'
defaultMessage='Home'
/></NavLink >,
<NavLink
className='tabs-bar__link'
to='/notifications'
data-preview-title-id='column.notifications'
data-preview-icon='bell'
><NotificationsCounterIcon /><FormattedMessage
id='tabs_bar.notifications'
defaultMessage='Notifications'
/></NavLink >,
<NavLink
className='tabs-bar__link'
to='/timelines/public/local'
data-preview-title-id='column.community'
data-preview-icon='users'
><Icon
id='users'
fixedWidth
/><FormattedMessage
id='tabs_bar.local_timeline'
defaultMessage='Local'
/></NavLink >,
<NavLink
className='tabs-bar__link'
exact
to='/timelines/public'
data-preview-title-id='column.public'
data-preview-icon='globe'
><Icon
id='globe'
fixedWidth
/><FormattedMessage
id='tabs_bar.federated_timeline'
defaultMessage='Federated'
/></NavLink >,
<NavLink
className='tabs-bar__link optional'
to='/search'
data-preview-title-id='tabs_bar.search'
data-preview-icon='bell'
><Icon
id='search'
fixedWidth
/><FormattedMessage
id='tabs_bar.search'
defaultMessage='Search'
/></NavLink >,
<NavLink
className='tabs-bar__link'
style={{ flexGrow: '0', flexBasis: '30px' }}
to='/getting-started'
data-preview-title-id='getting_started.heading'
data-preview-icon='bars'
><Icon
id='bars'
fixedWidth
/></NavLink >,
];
export function getIndex (path) {
export function getIndex(path) {
return links.findIndex(link => link.props.to === path);
}
export function getLink (index) {
export function getLink(index) {
return links[index].props.to;
}
@ -29,13 +91,13 @@ export default @injectIntl
class TabsBar extends React.PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
intl : PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
}
};
setRef = ref => {
this.node = ref;
}
};
handleClick = (e) => {
// Only apply optimization for touch devices, which we assume are slower
@ -50,7 +112,6 @@ class TabsBar extends React.PureComponent {
const nextTab = tabs.find(tab => tab.contains(e.target));
const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
if (currentTab !== nextTab) {
if (currentTab) {
currentTab.classList.remove('active');
@ -67,19 +128,28 @@ class TabsBar extends React.PureComponent {
});
}
}
};
render () {
render() {
const { intl: { formatMessage } } = this.props;
return (
<div className='tabs-bar__wrapper'>
<nav className='tabs-bar' ref={this.setRef}>
{links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
</nav>
<nav
className='tabs-bar'
ref={this.setRef}
>
{links.map(link => React.cloneElement(link, {
key : link.props.to,
onClick : this.handleClick,
'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }),
}))}
</nav >
<div id='tabs-bar__portal' />
</div>
</div >
);
}

View File

@ -10,112 +10,118 @@ import LoadingBarContainer from './containers/loading_bar_container';
import ModalContainer from './containers/modal_container';
import { isMobile } from '../../is_mobile';
import { debounce } from 'lodash';
import { uploadCompose, resetCompose } from '../../actions/compose';
import { resetCompose, uploadCompose } from '../../actions/compose';
import { expandHomeTimeline } from '../../actions/timelines';
import { expandNotifications } from '../../actions/notifications';
import { fetchFilters } from '../../actions/filters';
import { clearHeight } from '../../actions/height_cache';
import { focusApp, unfocusApp } from 'mastodon/actions/app';
import { submitMarkers } from 'mastodon/actions/markers';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import { WrappedRoute, WrappedSwitch } from './util/react_router_helpers';
import UploadArea from './components/upload_area';
import ColumnsAreaContainer from './containers/columns_area_container';
import DocumentTitle from './components/document_title';
import {
Compose,
Status,
GettingStarted,
KeyboardShortcuts,
PublicTimeline,
CommunityTimeline,
AccountTimeline,
AccountGallery,
HomeTimeline,
AccountTimeline,
Blocks,
BookmarkedStatuses,
CommunityTimeline,
Compose,
Directory,
DirectTimeline,
DomainBlocks,
FavouritedStatuses,
Favourites,
Followers,
Following,
Reblogs,
Favourites,
DirectTimeline,
HashtagTimeline,
Notifications,
FollowRequests,
GenericNotFound,
FavouritedStatuses,
BookmarkedStatuses,
ListTimeline,
Blocks,
DomainBlocks,
Mutes,
PinnedStatuses,
GettingStarted,
HashtagTimeline,
HomeTimeline,
KeyboardShortcuts,
Lists,
ListTimeline,
Mutes,
Notifications,
PinnedStatuses,
PublicTimeline,
Reblogs,
Search,
Directory,
Status,
} from './util/async-components';
import { me, forceSingleColumn } from '../../initial_state';
import { forceSingleColumn, me } from '../../initial_state';
import { previewState as previewMediaState } from './components/media_modal';
import { previewState as previewVideoState } from './components/video_modal';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
import '../../components/status';
import InstantMessaging from './components/messaging/instantMessaging';
const messages = defineMessages({
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
});
const mapStateToProps = state => ({
isComposing: state.getIn(['compose', 'is_composing']),
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
isComposing : state.getIn(['compose', 'is_composing']),
hasComposingText : state.getIn(['compose', 'text']).trim().length !== 0,
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
canUploadMore : !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
dropdownMenuIsOpen : state.getIn(['dropdown_menu', 'openId']) !== null,
});
const keyMap = {
help: '?',
new: 'n',
search: 's',
forceNew: 'option+n',
focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
reply: 'r',
favourite: 'f',
boost: 'b',
mention: 'm',
open: ['enter', 'o'],
openProfile: 'p',
moveDown: ['down', 'j'],
moveUp: ['up', 'k'],
back: 'backspace',
goToHome: 'g h',
help : '?',
new : 'n',
search : 's',
forceNew : 'option+n',
focusColumn : ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
reply : 'r',
favourite : 'f',
boost : 'b',
mention : 'm',
open : ['enter', 'o'],
openProfile : 'p',
moveDown : ['down', 'j'],
moveUp : ['up', 'k'],
back : 'backspace',
goToHome : 'g h',
goToNotifications: 'g n',
goToLocal: 'g l',
goToFederated: 'g t',
goToDirect: 'g d',
goToStart: 'g s',
goToFavourites: 'g f',
goToPinned: 'g p',
goToProfile: 'g u',
goToBlocked: 'g b',
goToMuted: 'g m',
goToRequests: 'g r',
toggleHidden: 'x',
toggleSensitive: 'h',
openMedia: 'e',
goToLocal : 'g l',
goToFederated : 'g t',
goToDirect : 'g d',
goToStart : 'g s',
goToFavourites : 'g f',
goToPinned : 'g p',
goToProfile : 'g u',
goToBlocked : 'g b',
goToMuted : 'g m',
goToRequests : 'g r',
toggleHidden : 'x',
toggleSensitive : 'h',
openMedia : 'e',
};
class SwitchingColumnsArea extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
location: PropTypes.object,
children : PropTypes.node,
location : PropTypes.object,
onLayoutChange: PropTypes.func.isRequired,
};
state = {
mobile: isMobile(window.innerWidth),
};
handleLayoutChange = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.onLayoutChange();
}, 500, {
trailing: true,
});
componentWillMount () {
componentWillMount() {
window.addEventListener('resize', this.handleResize, { passive: true });
if (this.state.mobile || forceSingleColumn) {
@ -127,7 +133,7 @@ class SwitchingColumnsArea extends React.PureComponent {
}
}
componentDidUpdate (prevProps, prevState) {
componentDidUpdate(prevProps, prevState) {
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
this.node.handleChildrenContentChange();
}
@ -138,21 +144,14 @@ class SwitchingColumnsArea extends React.PureComponent {
}
}
componentWillUnmount () {
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
shouldUpdateScroll (_, { location }) {
shouldUpdateScroll(_, { location }) {
return location.state !== previewMediaState && location.state !== previewVideoState;
}
handleLayoutChange = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.onLayoutChange();
}, 500, {
trailing: true,
})
handleResize = () => {
const mobile = isMobile(window.innerWidth);
@ -163,61 +162,219 @@ class SwitchingColumnsArea extends React.PureComponent {
} else {
this.handleLayoutChange();
}
}
};
setRef = c => {
if (c) {
this.node = c.getWrappedInstance();
}
}
};
render () {
render() {
const { children } = this.props;
const { mobile } = this.state;
const singleColumn = forceSingleColumn || mobile;
const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
const redirect = singleColumn ? (<Redirect
from='/'
to='/timelines/home'
exact
/>) : (<Redirect
from='/'
to='/getting-started'
exact
/>);
return (
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
<WrappedSwitch>
<ColumnsAreaContainer
ref={this.setRef}
singleColumn={singleColumn}
>
<WrappedSwitch >
{redirect}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute
path='/tk-example'
component={InstantMessaging}
content={children}
/>
<WrappedRoute
path='/getting-started'
component={GettingStarted}
content={children}
/>
<WrappedRoute
path='/keyboard-shortcuts'
component={KeyboardShortcuts}
content={children}
/>
<WrappedRoute
path='/timelines/home'
component={HomeTimeline}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/timelines/public'
exact
component={PublicTimeline}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/timelines/public/local'
exact
component={CommunityTimeline}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/timelines/direct'
component={DirectTimeline}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/timelines/tag/:id'
component={HashtagTimeline}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/timelines/list/:id'
component={ListTimeline}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute path='/notifications' component={Notifications} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute
path='/notifications'
component={Notifications}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/favourites'
component={FavouritedStatuses}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/bookmarks'
component={BookmarkedStatuses}
content={children}
/>
<WrappedRoute
path='/pinned'
component={PinnedStatuses}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute
path='/search'
component={Search}
content={children}
/>
<WrappedRoute
path='/directory'
component={Directory}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute
path='/statuses/new'
component={Compose}
content={children}
/>
<WrappedRoute
path='/statuses/:statusId'
exact
component={Status}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/statuses/:statusId/reblogs'
component={Reblogs}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/statuses/:statusId/favourites'
component={Favourites}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, withReplies: true }} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute
path='/accounts/:accountId'
exact
component={AccountTimeline}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/accounts/:accountId/with_replies'
component={AccountTimeline}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, withReplies: true }}
/>
<WrappedRoute
path='/accounts/:accountId/followers'
component={Followers}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/accounts/:accountId/following'
component={Following}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/accounts/:accountId/media'
component={AccountGallery}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/blocks' component={Blocks} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/mutes' component={Mutes} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/lists' component={Lists} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute
path='/follow_requests'
component={FollowRequests}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/blocks'
component={Blocks}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/domain_blocks'
component={DomainBlocks}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/mutes'
component={Mutes}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute
path='/lists'
component={Lists}
content={children}
componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }}
/>
<WrappedRoute component={GenericNotFound} content={children} />
</WrappedSwitch>
</ColumnsAreaContainer>
<WrappedRoute
component={GenericNotFound}
content={children}
/>
</WrappedSwitch >
</ColumnsAreaContainer >
);
}
@ -233,15 +390,15 @@ class UI extends React.PureComponent {
};
static propTypes = {
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
isComposing: PropTypes.bool,
hasComposingText: PropTypes.bool,
dispatch : PropTypes.func.isRequired,
children : PropTypes.node,
isComposing : PropTypes.bool,
hasComposingText : PropTypes.bool,
hasMediaAttachments: PropTypes.bool,
canUploadMore: PropTypes.bool,
location: PropTypes.object,
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
canUploadMore : PropTypes.bool,
location : PropTypes.object,
intl : PropTypes.object.isRequired,
dropdownMenuIsOpen : PropTypes.bool,
};
state = {
@ -259,20 +416,20 @@ class UI extends React.PureComponent {
// but we set user-friendly message for other browsers, e.g. Edge.
e.returnValue = intl.formatMessage(messages.beforeUnload);
}
}
};
handleWindowFocus = () => {
this.props.dispatch(focusApp());
}
};
handleWindowBlur = () => {
this.props.dispatch(unfocusApp());
}
};
handleLayoutChange = () => {
// The cached heights are no longer accurate, invalidate
this.props.dispatch(clearHeight());
}
};
handleDragEnter = (e) => {
e.preventDefault();
@ -288,7 +445,7 @@ class UI extends React.PureComponent {
if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore) {
this.setState({ draggingOver: true });
}
}
};
handleDragOver = (e) => {
if (this.dataTransferIsText(e.dataTransfer)) return false;
@ -303,7 +460,7 @@ class UI extends React.PureComponent {
}
return false;
}
};
handleDrop = (e) => {
if (this.dataTransferIsText(e.dataTransfer)) return;
@ -316,7 +473,7 @@ class UI extends React.PureComponent {
if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore) {
this.props.dispatch(uploadCompose(e.dataTransfer.files));
}
}
};
handleDragLeave = (e) => {
e.preventDefault();
@ -329,15 +486,15 @@ class UI extends React.PureComponent {
}
this.setState({ draggingOver: false });
}
};
dataTransferIsText = (dataTransfer) => {
return (dataTransfer && Array.from(dataTransfer.types).filter((type) => type === 'text/plain').length === 1);
}
};
closeUploadModal = () => {
this.setState({ draggingOver: false });
}
};
handleServiceWorkerPostMessage = ({ data }) => {
if (data.type === 'navigate') {
@ -345,9 +502,9 @@ class UI extends React.PureComponent {
} else {
console.warn('Unknown message type:', data.type);
}
}
};
componentWillMount () {
componentWillMount() {
window.addEventListener('focus', this.handleWindowFocus, false);
window.addEventListener('blur', this.handleWindowBlur, false);
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
@ -358,7 +515,7 @@ class UI extends React.PureComponent {
document.addEventListener('dragleave', this.handleDragLeave, false);
document.addEventListener('dragend', this.handleDragEnd, false);
if ('serviceWorker' in navigator) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
}
@ -372,13 +529,13 @@ class UI extends React.PureComponent {
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
}
componentDidMount () {
componentDidMount() {
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
};
}
componentWillUnmount () {
componentWillUnmount() {
window.removeEventListener('focus', this.handleWindowFocus);
window.removeEventListener('blur', this.handleWindowBlur);
window.removeEventListener('beforeunload', this.handleBeforeUnload);
@ -392,7 +549,7 @@ class UI extends React.PureComponent {
setRef = c => {
this.node = c;
}
};
handleHotkeyNew = e => {
e.preventDefault();
@ -402,7 +559,7 @@ class UI extends React.PureComponent {
if (element) {
element.focus();
}
}
};
handleHotkeySearch = e => {
e.preventDefault();
@ -412,15 +569,15 @@ class UI extends React.PureComponent {
if (element) {
element.focus();
}
}
};
handleHotkeyForceNew = e => {
this.handleHotkeyNew(e);
this.props.dispatch(resetCompose());
}
};
handleHotkeyFocusColumn = e => {
const index = (e.key * 1) + 1; // First child is drawer, skip that
const index = (e.key * 1) + 1; // First child is drawer, skip that
const column = this.node.querySelector(`.column:nth-child(${index})`);
if (!column) return;
const container = column.querySelector('.scrollable');
@ -435,7 +592,7 @@ class UI extends React.PureComponent {
status.focus();
}
}
}
};
handleHotkeyBack = () => {
if (window.history && window.history.length === 1) {
@ -443,11 +600,11 @@ class UI extends React.PureComponent {
} else {
this.context.router.history.goBack();
}
}
};
setHotkeysRef = c => {
this.hotkeys = c;
}
};
handleHotkeyToggleHelp = () => {
if (this.props.location.pathname === '/keyboard-shortcuts') {
@ -455,95 +612,111 @@ class UI extends React.PureComponent {
} else {
this.context.router.history.push('/keyboard-shortcuts');
}
}
};
handleHotkeyGoToHome = () => {
this.context.router.history.push('/timelines/home');
}
};
handleHotkeyGoToNotifications = () => {
this.context.router.history.push('/notifications');
}
};
handleHotkeyGoToLocal = () => {
this.context.router.history.push('/timelines/public/local');
}
};
handleHotkeyGoToFederated = () => {
this.context.router.history.push('/timelines/public');
}
};
handleHotkeyGoToDirect = () => {
this.context.router.history.push('/timelines/direct');
}
};
handleHotkeyGoToStart = () => {
this.context.router.history.push('/getting-started');
}
};
handleHotkeyGoToFavourites = () => {
this.context.router.history.push('/favourites');
}
};
handleHotkeyGoToPinned = () => {
this.context.router.history.push('/pinned');
}
};
handleHotkeyGoToProfile = () => {
this.context.router.history.push(`/accounts/${me}`);
}
};
handleHotkeyGoToBlocked = () => {
this.context.router.history.push('/blocks');
}
};
handleHotkeyGoToMuted = () => {
this.context.router.history.push('/mutes');
}
};
handleHotkeyGoToRequests = () => {
this.context.router.history.push('/follow_requests');
}
};
render () {
render() {
const { draggingOver } = this.state;
const { children, isComposing, location, dropdownMenuIsOpen } = this.props;
const handlers = {
help: this.handleHotkeyToggleHelp,
new: this.handleHotkeyNew,
search: this.handleHotkeySearch,
forceNew: this.handleHotkeyForceNew,
focusColumn: this.handleHotkeyFocusColumn,
back: this.handleHotkeyBack,
goToHome: this.handleHotkeyGoToHome,
help : this.handleHotkeyToggleHelp,
new : this.handleHotkeyNew,
search : this.handleHotkeySearch,
forceNew : this.handleHotkeyForceNew,
focusColumn : this.handleHotkeyFocusColumn,
back : this.handleHotkeyBack,
goToHome : this.handleHotkeyGoToHome,
goToNotifications: this.handleHotkeyGoToNotifications,
goToLocal: this.handleHotkeyGoToLocal,
goToFederated: this.handleHotkeyGoToFederated,
goToDirect: this.handleHotkeyGoToDirect,
goToStart: this.handleHotkeyGoToStart,
goToFavourites: this.handleHotkeyGoToFavourites,
goToPinned: this.handleHotkeyGoToPinned,
goToProfile: this.handleHotkeyGoToProfile,
goToBlocked: this.handleHotkeyGoToBlocked,
goToMuted: this.handleHotkeyGoToMuted,
goToRequests: this.handleHotkeyGoToRequests,
goToLocal : this.handleHotkeyGoToLocal,
goToFederated : this.handleHotkeyGoToFederated,
goToDirect : this.handleHotkeyGoToDirect,
goToStart : this.handleHotkeyGoToStart,
goToFavourites : this.handleHotkeyGoToFavourites,
goToPinned : this.handleHotkeyGoToPinned,
goToProfile : this.handleHotkeyGoToProfile,
goToBlocked : this.handleHotkeyGoToBlocked,
goToMuted : this.handleHotkeyGoToMuted,
goToRequests : this.handleHotkeyGoToRequests,
};
return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}>
<HotKeys
keyMap={keyMap}
handlers={handlers}
ref={this.setHotkeysRef}
attach={window}
focused
>
<div
className={classNames('ui', { 'is-composing': isComposing })}
ref={this.setRef}
style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}
>
<SwitchingColumnsArea
location={location}
onLayoutChange={this.handleLayoutChange}
>
{children}
</SwitchingColumnsArea>
</SwitchingColumnsArea >
<NotificationsContainer />
<LoadingBarContainer className='loading-bar' />
<ModalContainer />
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
<UploadArea
active={draggingOver}
onClose={this.closeUploadModal}
/>
<DocumentTitle />
</div>
</HotKeys>
</div >
</HotKeys >
);
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Switch, Route } from 'react-router-dom';
import { Route, Switch } from 'react-router-dom';
import ColumnLoading from '../components/column_loading';
import BundleColumnError from '../components/bundle_column_error';
@ -9,13 +9,13 @@ import BundleContainer from '../containers/bundle_container';
// Small wrapper to pass multiColumn to the route components
export class WrappedSwitch extends React.PureComponent {
render () {
render() {
const { multiColumn, children } = this.props;
return (
<Switch>
<Switch >
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
</Switch>
</Switch >
);
}
@ -23,7 +23,7 @@ export class WrappedSwitch extends React.PureComponent {
WrappedSwitch.propTypes = {
multiColumn: PropTypes.bool,
children: PropTypes.node,
children : PropTypes.node,
};
// Small Wrapper to extract the params from the route and pass
@ -32,9 +32,9 @@ WrappedSwitch.propTypes = {
export class WrappedRoute extends React.Component {
static propTypes = {
component: PropTypes.func.isRequired,
content: PropTypes.node,
multiColumn: PropTypes.bool,
component : PropTypes.func.isRequired,
content : PropTypes.node,
multiColumn : PropTypes.bool,
componentParams: PropTypes.object,
};
@ -46,21 +46,28 @@ export class WrappedRoute extends React.Component {
const { component, content, multiColumn, componentParams } = this.props;
return (
<BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
{Component => <Component params={match.params} multiColumn={multiColumn} {...componentParams}>{content}</Component>}
</BundleContainer>
<BundleContainer
fetchComponent={component}
loading={this.renderLoading}
error={this.renderError}
>
{Component => (<Component
params={match.params}
multiColumn={multiColumn} {...componentParams}
>{content}</Component >)}
</BundleContainer >
);
}
};
renderLoading = () => {
return <ColumnLoading />;
}
};
renderError = (props) => {
return <BundleColumnError {...props} />;
}
};
render () {
render() {
const { component: Component, content, ...rest } = this.props;
return <Route {...rest} render={this.renderComponent} />;

View File

@ -15,9 +15,9 @@ class ReducedMotion extends React.Component {
static propTypes = {
defaultStyle: PropTypes.object,
style: PropTypes.object,
children: PropTypes.func,
}
style : PropTypes.object,
children : PropTypes.func,
};
render() {
@ -33,9 +33,12 @@ class ReducedMotion extends React.Component {
});
return (
<Motion style={style} defaultStyle={defaultStyle}>
<Motion
style={style}
defaultStyle={defaultStyle}
>
{children}
</Motion>
</Motion >
);
}

View File

@ -25,5 +25,5 @@ export const usePendingItems = getMeta('use_pending_items');
export const showTrends = getMeta('trends');
export const title = getMeta('title');
export const cropImages = getMeta('crop_images');
console.log('initialState', initialState);
export default initialState;

View File

@ -5,7 +5,7 @@
"account.block_domain": "Tout masquer venant de {domain}",
"account.blocked": "Bloqué·e",
"account.cancel_follow_request": "Annuler la demande de suivi",
"account.direct": "Envoyer un message direct à @{name}",
"account.direct": "Envoyer un message privé à @{name}",
"account.domain_blocked": "Domaine caché",
"account.edit_profile": "Modifier le profil",
"account.endorse": "Recommander sur le profil",
@ -52,6 +52,7 @@
"bundle_modal_error.retry": "Réessayer",
"column.bookmarks": "Marque pages",
"column.blocks": "Comptes bloqués",
"column.bookmarks": "Bookmarks",
"column.community": "Fil public local",
"column.direct": "Messages privés",
"column.directory": "Parcourir les profils",
@ -139,8 +140,9 @@
"empty_column.account_timeline": "Aucun pouet ici !",
"empty_column.account_unavailable": "Profil non disponible",
"empty_column.blocks": "Vous navez bloqué aucun·e utilisateur·rice pour le moment.",
"empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
"empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir!",
"empty_column.direct": "Vous navez pas encore de messages directs. Lorsque vous en enverrez ou recevrez un, il saffichera ici.",
"empty_column.direct": "Vous navez pas encore de messages privés. Lorsque vous en enverrez ou recevrez un, il saffichera ici.",
"empty_column.domain_blocks": "Il ny a aucun domaine caché pour le moment.",
"empty_column.favourited_statuses": "Vous navez aucun pouet favoris pour le moment. Lorsque vous en mettrez un en favori, il apparaîtra ici.",
"empty_column.favourites": "Personne na encore mis ce pouet en favori. Lorsque quelquun le fera, il apparaîtra ici.",
@ -164,7 +166,7 @@
"getting_started.documentation": "Documentation",
"getting_started.heading": "Pour commencer",
"getting_started.invite": "Inviter des gens",
"getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer ou faire des rapports de bogues via {github} sur GitHub.",
"getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer ou faire des rapports de bogues via {forge} sur GitHub.",
"getting_started.security": "Sécurité",
"getting_started.terms": "Conditions dutilisation",
"hashtag.column_header.tag_mode.all": "et {additional}",
@ -205,7 +207,7 @@
"keyboard_shortcuts.column": "pour focaliser un statut dans lune des colonnes",
"keyboard_shortcuts.compose": "pour focaliser la zone de rédaction",
"keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.direct": "pour ouvrir la colonne des messages directs",
"keyboard_shortcuts.direct": "pour ouvrir la colonne des messages privés",
"keyboard_shortcuts.down": "pour descendre dans la liste",
"keyboard_shortcuts.enter": "pour ouvrir le statut",
"keyboard_shortcuts.favourite": "pour ajouter aux favoris",
@ -253,9 +255,10 @@
"mute_modal.hide_notifications": "Masquer les notifications de cette personne?",
"navigation_bar.apps": "Applications mobiles",
"navigation_bar.blocks": "Comptes bloqués",
"navigation_bar.bookmarks": "Bookmarks",
"navigation_bar.community_timeline": "Fil public local",
"navigation_bar.compose": "Rédiger un nouveau pouet",
"navigation_bar.direct": "Messages directs",
"navigation_bar.direct": "Messages privés",
"navigation_bar.discover": "Découvrir",
"navigation_bar.domain_blocks": "Domaines cachés",
"navigation_bar.edit_profile": "Modifier le profil",
@ -352,7 +355,7 @@
"status.copy": "Copier le lien vers le pouet",
"status.delete": "Effacer",
"status.detailed_status": "Vue détaillée de la conversation",
"status.direct": "Envoyer un message direct à @{name}",
"status.direct": "Envoyer un message privé à @{name}",
"status.embed": "Intégrer",
"status.favourite": "Ajouter aux favoris",
"status.filtered": "Filtré",
@ -371,6 +374,7 @@
"status.reblogged_by": "{name} a partagé:",
"status.reblogs.empty": "Personne na encore partagé ce pouet. Lorsque quelquun le fera, il apparaîtra ici.",
"status.redraft": "Effacer et ré-écrire",
"status.remove_bookmark": "Enlever le marque-page",
"status.reply": "Répondre",
"status.replyAll": "Répondre au fil",
"status.report": "Signaler @{name}",
@ -386,9 +390,9 @@
"status.unpin": "Retirer du profil",
"suggestions.dismiss": "Rejeter la suggestion",
"suggestions.header": "Vous pourriez être intéressé·e par…",
"tabs_bar.federated_timeline": "Fil public global",
"tabs_bar.federated_timeline": "Global",
"tabs_bar.home": "Accueil",
"tabs_bar.local_timeline": "Fil public local",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications",
"tabs_bar.search": "Chercher",
"time_remaining.days": "{number, plural, one {# day} other {# days}} restants",

View File

@ -1,5 +1,5 @@
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
import { Map as ImmutableMap, fromJS } from 'immutable';
import { fromJS, Map as ImmutableMap } from 'immutable';
const initialState = ImmutableMap();
@ -22,7 +22,7 @@ const normalizeAccounts = (state, accounts) => {
};
export default function accounts(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case ACCOUNT_IMPORT:
return normalizeAccount(state, action.account);
case ACCOUNTS_IMPORT:

View File

@ -1,21 +1,16 @@
import {
REBLOG_REQUEST,
REBLOG_FAIL,
FAVOURITE_REQUEST,
FAVOURITE_FAIL,
UNFAVOURITE_SUCCESS,
BOOKMARK_REQUEST,
BOOKMARK_FAIL,
BOOKMARK_REQUEST,
FAVOURITE_FAIL,
FAVOURITE_REQUEST,
REBLOG_FAIL,
REBLOG_REQUEST,
UNFAVOURITE_SUCCESS,
} from '../actions/interactions';
import {
STATUS_MUTE_SUCCESS,
STATUS_UNMUTE_SUCCESS,
STATUS_REVEAL,
STATUS_HIDE,
} from '../actions/statuses';
import { STATUS_HIDE, STATUS_MUTE_SUCCESS, STATUS_REVEAL, STATUS_UNMUTE_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { Map as ImmutableMap, fromJS } from 'immutable';
import { fromJS, Map as ImmutableMap } from 'immutable';
const importStatus = (state, status) => state.set(status.id, fromJS(status));
@ -33,7 +28,7 @@ const deleteStatus = (state, id, references) => {
const initialState = ImmutableMap();
export default function statuses(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case STATUS_IMPORT:
return importStatus(state, action.status);
case STATUSES_IMPORT:

View File

@ -0,0 +1,29 @@
/** @license
DHTML Snowstorm! JavaScript-based snow for web pages
Making it snow on the internets since 2003. You're welcome.
-----------------------------------------------------------
Version 1.44.20131208 (Previous rev: 1.44.20131125)
Copyright (c) 2007, Scott Schiller. All rights reserved.
Code provided under the BSD License
http://schillmania.com/projects/snowstorm/license.txt
*/
var snowStorm=function(g,f){function k(a,d){isNaN(d)&&(d=0);return Math.random()*a+d}function x(){g.setTimeout(function(){a.start(!0)},20);a.events.remove(m?f:g,"mousemove",x)}function y(){(!a.excludeMobile||!D)&&x();a.events.remove(g,"load",y)}this.excludeMobile=this.autoStart=!0;this.flakesMax=128;this.flakesMaxActive=64;this.animationInterval=33;this.useGPU=!0;this.className=null;this.excludeMobile=!0;this.flakeBottom=null;this.followMouse=!0;this.snowColor="#fff";this.snowCharacter="&bull;";this.snowStick=
!0;this.targetElement=null;this.useMeltEffect=!0;this.usePixelPosition=this.usePositionFixed=this.useTwinkleEffect=!1;this.freezeOnBlur=!0;this.flakeRightOffset=this.flakeLeftOffset=0;this.flakeHeight=this.flakeWidth=8;this.vMaxX=5;this.vMaxY=4;this.zIndex=0;var a=this,q,m=navigator.userAgent.match(/msie/i),E=navigator.userAgent.match(/msie 6/i),D=navigator.userAgent.match(/mobile|opera m(ob|in)/i),r=m&&"BackCompat"===f.compatMode||E,h=null,n=null,l=null,p=null,s=null,z=null,A=null,v=1,t=!1,w=!1,
u;a:{try{f.createElement("div").style.opacity="0.5"}catch(F){u=!1;break a}u=!0}var B=!1,C=f.createDocumentFragment();q=function(){function c(b){g.setTimeout(b,1E3/(a.animationInterval||20))}function d(a){return void 0!==h.style[a]?a:null}var e,b=g.requestAnimationFrame||g.webkitRequestAnimationFrame||g.mozRequestAnimationFrame||g.oRequestAnimationFrame||g.msRequestAnimationFrame||c;e=b?function(){return b.apply(g,arguments)}:null;var h;h=f.createElement("div");e={transform:{ie:d("-ms-transform"),
moz:d("MozTransform"),opera:d("OTransform"),webkit:d("webkitTransform"),w3:d("transform"),prop:null},getAnimationFrame:e};e.transform.prop=e.transform.w3||e.transform.moz||e.transform.webkit||e.transform.ie||e.transform.opera;h=null;return e}();this.timer=null;this.flakes=[];this.active=this.disabled=!1;this.meltFrameCount=20;this.meltFrames=[];this.setXY=function(c,d,e){if(!c)return!1;a.usePixelPosition||w?(c.style.left=d-a.flakeWidth+"px",c.style.top=e-a.flakeHeight+"px"):r?(c.style.right=100-100*
(d/h)+"%",c.style.top=Math.min(e,s-a.flakeHeight)+"px"):a.flakeBottom?(c.style.right=100-100*(d/h)+"%",c.style.top=Math.min(e,s-a.flakeHeight)+"px"):(c.style.right=100-100*(d/h)+"%",c.style.bottom=100-100*(e/l)+"%")};this.events=function(){function a(c){c=b.call(c);var d=c.length;e?(c[1]="on"+c[1],3<d&&c.pop()):3===d&&c.push(!1);return c}function d(a,b){var c=a.shift(),d=[f[b]];if(e)c[d](a[0],a[1]);else c[d].apply(c,a)}var e=!g.addEventListener&&g.attachEvent,b=Array.prototype.slice,f={add:e?"attachEvent":
"addEventListener",remove:e?"detachEvent":"removeEventListener"};return{add:function(){d(a(arguments),"add")},remove:function(){d(a(arguments),"remove")}}}();this.randomizeWind=function(){var c;c=k(a.vMaxX,0.2);z=1===parseInt(k(2),10)?-1*c:c;A=k(a.vMaxY,0.2);if(this.flakes)for(c=0;c<this.flakes.length;c++)this.flakes[c].active&&this.flakes[c].setVelocities()};this.scrollHandler=function(){var c;p=a.flakeBottom?0:parseInt(g.scrollY||f.documentElement.scrollTop||(r?f.body.scrollTop:0),10);isNaN(p)&&
(p=0);if(!t&&!a.flakeBottom&&a.flakes)for(c=0;c<a.flakes.length;c++)0===a.flakes[c].active&&a.flakes[c].stick()};this.resizeHandler=function(){g.innerWidth||g.innerHeight?(h=g.innerWidth-16-a.flakeRightOffset,l=a.flakeBottom||g.innerHeight):(h=(f.documentElement.clientWidth||f.body.clientWidth||f.body.scrollWidth)-(!m?8:0)-a.flakeRightOffset,l=a.flakeBottom||f.documentElement.clientHeight||f.body.clientHeight||f.body.scrollHeight);s=f.body.offsetHeight;n=parseInt(h/2,10)};this.resizeHandlerAlt=function(){h=
a.targetElement.offsetWidth-a.flakeRightOffset;l=a.flakeBottom||a.targetElement.offsetHeight;n=parseInt(h/2,10);s=f.body.offsetHeight};this.freeze=function(){if(a.disabled)return!1;a.disabled=1;a.timer=null};this.resume=function(){if(a.disabled)a.disabled=0;else return!1;a.timerInit()};this.toggleSnow=function(){a.flakes.length?(a.active=!a.active,a.active?(a.show(),a.resume()):(a.stop(),a.freeze())):a.start()};this.stop=function(){var c;this.freeze();for(c=0;c<this.flakes.length;c++)this.flakes[c].o.style.display=
"none";a.events.remove(g,"scroll",a.scrollHandler);a.events.remove(g,"resize",a.resizeHandler);a.freezeOnBlur&&(m?(a.events.remove(f,"focusout",a.freeze),a.events.remove(f,"focusin",a.resume)):(a.events.remove(g,"blur",a.freeze),a.events.remove(g,"focus",a.resume)))};this.show=function(){var a;for(a=0;a<this.flakes.length;a++)this.flakes[a].o.style.display="block"};this.SnowFlake=function(c,d,e){var b=this;this.type=c;this.x=d||parseInt(k(h-20),10);this.y=!isNaN(e)?e:-k(l)-12;this.vY=this.vX=null;
this.vAmpTypes=[1,1.2,1.4,1.6,1.8];this.vAmp=this.vAmpTypes[this.type]||1;this.melting=!1;this.meltFrameCount=a.meltFrameCount;this.meltFrames=a.meltFrames;this.twinkleFrame=this.meltFrame=0;this.active=1;this.fontSize=10+10*(this.type/5);this.o=f.createElement("div");this.o.innerHTML=a.snowCharacter;a.className&&this.o.setAttribute("class",a.className);this.o.style.color=a.snowColor;this.o.style.position=t?"fixed":"absolute";a.useGPU&&q.transform.prop&&(this.o.style[q.transform.prop]="translate3d(0px, 0px, 0px)");
this.o.style.width=a.flakeWidth+"px";this.o.style.height=a.flakeHeight+"px";this.o.style.fontFamily="arial,verdana";this.o.style.cursor="default";this.o.style.overflow="hidden";this.o.style.fontWeight="normal";this.o.style.zIndex=a.zIndex;C.appendChild(this.o);this.refresh=function(){if(isNaN(b.x)||isNaN(b.y))return!1;a.setXY(b.o,b.x,b.y)};this.stick=function(){r||a.targetElement!==f.documentElement&&a.targetElement!==f.body?b.o.style.top=l+p-a.flakeHeight+"px":a.flakeBottom?b.o.style.top=a.flakeBottom+
"px":(b.o.style.display="none",b.o.style.bottom="0%",b.o.style.position="fixed",b.o.style.display="block")};this.vCheck=function(){0<=b.vX&&0.2>b.vX?b.vX=0.2:0>b.vX&&-0.2<b.vX&&(b.vX=-0.2);0<=b.vY&&0.2>b.vY&&(b.vY=0.2)};this.move=function(){var c=b.vX*v;b.x+=c;b.y+=b.vY*b.vAmp;b.x>=h||h-b.x<a.flakeWidth?b.x=0:0>c&&b.x-a.flakeLeftOffset<-a.flakeWidth&&(b.x=h-a.flakeWidth-1);b.refresh();l+p-b.y+a.flakeHeight<a.flakeHeight?(b.active=0,a.snowStick?b.stick():b.recycle()):(a.useMeltEffect&&(b.active&&3>
b.type&&!b.melting&&0.998<Math.random())&&(b.melting=!0,b.melt()),a.useTwinkleEffect&&(0>b.twinkleFrame?0.97<Math.random()&&(b.twinkleFrame=parseInt(8*Math.random(),10)):(b.twinkleFrame--,u?b.o.style.opacity=b.twinkleFrame&&0===b.twinkleFrame%2?0:1:b.o.style.visibility=b.twinkleFrame&&0===b.twinkleFrame%2?"hidden":"visible")))};this.animate=function(){b.move()};this.setVelocities=function(){b.vX=z+k(0.12*a.vMaxX,0.1);b.vY=A+k(0.12*a.vMaxY,0.1)};this.setOpacity=function(a,b){if(!u)return!1;a.style.opacity=
b};this.melt=function(){!a.useMeltEffect||!b.melting?b.recycle():b.meltFrame<b.meltFrameCount?(b.setOpacity(b.o,b.meltFrames[b.meltFrame]),b.o.style.fontSize=b.fontSize-b.fontSize*(b.meltFrame/b.meltFrameCount)+"px",b.o.style.lineHeight=a.flakeHeight+2+0.75*a.flakeHeight*(b.meltFrame/b.meltFrameCount)+"px",b.meltFrame++):b.recycle()};this.recycle=function(){b.o.style.display="none";b.o.style.position=t?"fixed":"absolute";b.o.style.bottom="auto";b.setVelocities();b.vCheck();b.meltFrame=0;b.melting=
!1;b.setOpacity(b.o,1);b.o.style.padding="0px";b.o.style.margin="0px";b.o.style.fontSize=b.fontSize+"px";b.o.style.lineHeight=a.flakeHeight+2+"px";b.o.style.textAlign="center";b.o.style.verticalAlign="baseline";b.x=parseInt(k(h-a.flakeWidth-20),10);b.y=parseInt(-1*k(l),10)-a.flakeHeight;b.refresh();b.o.style.display="block";b.active=1};this.recycle();this.refresh()};this.snow=function(){var c=0,d=null,e,d=0;for(e=a.flakes.length;d<e;d++)1===a.flakes[d].active&&(a.flakes[d].move(),c++),a.flakes[d].melting&&
a.flakes[d].melt();c<a.flakesMaxActive&&(d=a.flakes[parseInt(k(a.flakes.length),10)],0===d.active&&(d.melting=!0));a.timer&&q.getAnimationFrame(a.snow)};this.mouseMove=function(c){if(!a.followMouse)return!0;c=parseInt(c.clientX,10);c<n?v=-2+2*(c/n):(c-=n,v=2*(c/n))};this.createSnow=function(c,d){var e;for(e=0;e<c;e++)if(a.flakes[a.flakes.length]=new a.SnowFlake(parseInt(k(6),10)),d||e>a.flakesMaxActive)a.flakes[a.flakes.length-1].active=-1;a.targetElement.appendChild(C)};this.timerInit=function(){a.timer=
!0;a.snow()};this.init=function(){var c;for(c=0;c<a.meltFrameCount;c++)a.meltFrames.push(1-c/a.meltFrameCount);a.randomizeWind();a.createSnow(a.flakesMax);a.events.add(g,"resize",a.resizeHandler);a.events.add(g,"scroll",a.scrollHandler);a.freezeOnBlur&&(m?(a.events.add(f,"focusout",a.freeze),a.events.add(f,"focusin",a.resume)):(a.events.add(g,"blur",a.freeze),a.events.add(g,"focus",a.resume)));a.resizeHandler();a.scrollHandler();a.followMouse&&a.events.add(m?f:g,"mousemove",a.mouseMove);a.animationInterval=
Math.max(20,a.animationInterval);a.timerInit()};this.start=function(c){if(B){if(c)return!0}else B=!0;if("string"===typeof a.targetElement&&(c=a.targetElement,a.targetElement=f.getElementById(c),!a.targetElement))throw Error('Snowstorm: Unable to get targetElement "'+c+'"');a.targetElement||(a.targetElement=f.body||f.documentElement);a.targetElement!==f.documentElement&&a.targetElement!==f.body&&(a.resizeHandler=a.resizeHandlerAlt,a.usePixelPosition=!0);a.resizeHandler();a.usePositionFixed=a.usePositionFixed&&
!r&&!a.flakeBottom;if(g.getComputedStyle)try{w="relative"===g.getComputedStyle(a.targetElement,null).getPropertyValue("position")}catch(d){w=!1}t=a.usePositionFixed;h&&(l&&!a.disabled)&&(a.init(),a.active=!0)};a.autoStart&&a.events.add(g,"load",y,!1);return this}(window, document);

View File

@ -45,7 +45,25 @@ const onDomainBlockSeverityChange = (target) => {
delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));
const onEnableBootstrapTimelineAccountsChange = (target) => {
const bootstrapTimelineAccountsField = document.querySelector('#form_admin_settings_bootstrap_timeline_accounts');
if (bootstrapTimelineAccountsField) {
bootstrapTimelineAccountsField.disabled = !target.checked;
if (target.checked) {
bootstrapTimelineAccountsField.parentElement.classList.remove('disabled');
} else {
bootstrapTimelineAccountsField.parentElement.classList.add('disabled');
}
}
};
delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
ready(() => {
const input = document.getElementById('domain_block_severity');
if (input) onDomainBlockSeverityChange(input);
const domainBlockSeverityInput = document.getElementById('domain_block_severity');
if (domainBlockSeverityInput) onDomainBlockSeverityChange(domainBlockSeverityInput);
const enableBootstrapTimelineAccounts = document.getElementById('form_admin_settings_enable_bootstrap_timeline_accounts');
if (enableBootstrapTimelineAccounts) onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
});

View File

@ -1,3 +1,6 @@
@import '~bulma/sass/utilities/_all';
@import '~bulma/sass/grid/columns';
@import 'bliss/variables';
@import 'bliss/mixins';
@import 'bliss/variables';

View File

@ -72,3 +72,13 @@
color: $inverted-text-color;
}
}
.hidden {
display: none;
&:before {
content: "hidden div here";
background: yellow;
padding: 1em;
}
}

View File

@ -1,7 +1,3 @@
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
$column-breakpoint: 700px;
$small-breakpoint: 960px;
.container {
box-sizing: border-box;
@ -622,7 +618,7 @@ $small-breakpoint: 960px;
}
}
@media screen and (max-width: 675px) {
@media screen and (max-width: $xs-breakpoint) {
.header-wrapper {
padding-top: 0;
@ -649,7 +645,7 @@ $small-breakpoint: 960px;
.landing {
margin-bottom: 100px;
@media screen and (max-width: 738px) {
@media screen and (max-width: $xs-top-breakpoint) {
margin-bottom: 0;
}
@ -715,9 +711,9 @@ $small-breakpoint: 960px;
}
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
width: $avatar-side;
height: $avatar-side;
background-size: $avatar-side $avatar-side;
}
}
@ -752,13 +748,13 @@ $small-breakpoint: 960px;
}
&__grid {
max-width: 960px;
max-width: $small-breakpoint;
margin: 0 auto;
display: grid;
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
grid-gap: 30px;
@media screen and (max-width: 738px) {
@media screen and (max-width: $xs-top-breakpoint) {
grid-template-columns: minmax(0, 100%);
grid-gap: 10px;

View File

@ -13,6 +13,4 @@ $black-emojis: '8ball' 'ant' 'back' 'black_circle' 'black_heart' 'black_large_sq
}
}
.fa {
margin-right: 1ch;
}

View File

@ -384,6 +384,22 @@ hr.spacer {
max-width: 100%;
}
input {
background: auto;
}
hr,
.select select,
.textarea,
.input {
background-color: transparent;
border-color: transparent;
&:hover {
border: transparent;
}
}
.batch-form-box {
display: flex;
flex-wrap: wrap;

View File

@ -160,3 +160,35 @@ button {
height: 100%;
}
}
.media-gallery {
margin-left: 0;
}
.debug, .well {
padding: 0.5rem;
border: solid 1px greenyellow;
background: yellow;
color: #222;
}
.spacer {
display: block;
padding: 1em;
}
.small-texts {
.timelines {
.column-link {
}
span {
display: none;
}
}
}

View File

@ -406,6 +406,10 @@
}
}
.autosuggest-textarea__textarea {
margin-bottom: 5px;
}
.autosuggest-textarea__textarea,
.spoiler-input__input {
display: block;
@ -2193,27 +2197,16 @@ a.account__display-name {
}
.notification {
&__message {
margin-left: 48px + 15px * 2;
padding-top: 15px;
}
&__favourite-icon-wrapper {
left: -32px;
}
.status {
padding-top: 8px;
}
background-color: $classic-base-color !important;
padding: 1em 0;
color: $ui-highlight-color;
border-left: $ui-highlight-color 5px solid;
.account {
padding: 0;
padding-top: 8px;
}
.account__avatar-wrapper {
margin-left: 17px;
margin-right: 15px;
}
}
}
}
@ -2261,6 +2254,7 @@ a.account__display-name {
.columns-area__panels__pane--compositional {
display: none;
}
}
@media screen and (min-width: 600px + (285px * 1) + (10px * 1)) {
@ -2315,6 +2309,7 @@ a.account__display-name {
flex-direction: column;
height: calc(100% - 10px);
overflow-y: hidden;
z-index: 1;
.navigation-bar {
padding-top: 20px;
@ -2659,6 +2654,28 @@ a.account__display-name {
padding: 15px;
text-decoration: none;
.timelines & {
display: inline-block;
width: 20%;
display: inline-block !important;
float: left;
text-align: center;
span {
opacity: 0;
transition: opacity ease-in 1s;
}
&:hover {
span {
opacity: 1;
transition: opacity ease-in 0.2s;
}
}
}
&:hover,
&:focus,
&:active {

View File

@ -136,7 +136,23 @@
}
}
.getting-started__footer {
.links-started__footer {
display: block;
position: fixed;
bottom: 0;
left: 1em;
width: 300px;
z-index: 10;
text-align: right;
color: $ui-primary-color;
hr {
border-color: $ui-primary-color;
}
.fa {
margin-right: 1ch;
}
ul {
list-style-type: none;
@ -150,4 +166,36 @@
i {
margin: 0.5ch;
}
a {
margin-left: 2ch;
color: $ui-primary-color;
padding: 3px;
border-radius: 3px;
&:hover {
color: white;
background: $ui-highlight-color;
}
}
@media all and(min-width: $maximum-width) {
& {
width: 25vw;
}
}
@media all and(max-width: $maximum-width) {
& {
width: 22vw;
height: 50vh;
overflow-y: auto;
padding-right: 1em;
}
}
@media all and(max-width: $small-breakpoint) {
& {
display: none;
}
}
}

View File

@ -1,35 +1,41 @@
$no-columns-breakpoint: 600px;
table{
thead{
table {
thead {
th{
th {
font-weight: 800;
background: $ui-highlight-color;
}
}
td, th{
padding: 1rem;
}
a{
td, th {
padding: 1rem;
}
a {
@extend .text-btn
}
}
.table-responsive{
.table-responsive {
width: 100%;
}
.table-striped{
.table-striped {
margin: 1rem 0;
tr{
tr {
&:odd{
&:odd {
background: $ui-base-lighter-color;
}
}
}
.group-form{
.group-form {
}
code {
font-family: $font-monospace, monospace;
font-weight: 400;
@ -114,6 +120,7 @@ code {
position: absolute;
margin: 0 4px;
margin-top: -2px;
background: $ui-highlight-color;
}
}
}
@ -957,3 +964,8 @@ code {
flex-direction: row;
}
}
.compose-form__publish-button-wrapper {
width: 100%;
display: block;
}

View File

@ -1,84 +1 @@
$messagingBoxWidth: 15em;
$messagingBoxHeight: 20em;
.fixed-box {
border: solid 1px white;
padding: 1em;
position: fixed;
bottom: 0;
}
.messaging-box {
@extend .fixed-box;
right: 1em;
width: $messagingBoxWidth;
background: $ui-base-color;
}
.conversations_list {
@extend .fixed-box;
width: $messagingBoxWidth;
right: $messagingBoxWidth + 5em;
background: $ui-secondary-color;
}
.conversation-item {
&.has-new-message {
background: $ui-highlight-color;
color: $classic-primary-color;
}
}
.conversation_created-at {
margin-right: 1em;
}
.conversation_stream {
padding-top: 1em;
height: $messagingBoxHeight;
overflow: auto;
background: $ui-secondary-color;
.message {
-webkit-border-radius: 0.5rem;
-moz-border-radius: 0.5rem;
border-radius: 0.5rem;
margin-bottom: 0.5em;
padding: 0.5em 1em;
width: 80%;
}
.mine {
text-align: right;
background: $classic-primary-color;
float: right;
.arrow-down {
border-top-color: $classic-primary-color;
left: 1em;
}
}
.theirs {
text-align: left;
background: $ui-highlight-color;
float: left;
.arrow-down {
border-top-color: $ui-highlight-color;
right: 1em;
}
}
.arrow-down {
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 20px solid transparent;
border-top: 20px solid $classic-primary-color;
position: relative;
bottom: -1em;
}
}
@import "../mastodon/messaging/main";

View File

@ -1,16 +1,16 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$black: #111; // Black
$white: #fff; // White
$success-green: #6bbd77 !default; // Padua
$error-red: #d4839b !default; // Cerise
$warning-red: #528dc8 !default; // Sunset Orange
$gold-star: #98c6ff !default; // Dark Goldenrod
// Values from the classic Mastodon UI
$classic-base-color: #282c37; // Midnight Express
$classic-primary-color: #9baec8; // Echo Blue
$classic-secondary-color: #d9e1e8; // Pattens Blue
$classic-highlight-color: #2b90d9; // Summer Sky
$classic-base-color: #282c37; // Midnight Express
$classic-primary-color: #9baec8; // Echo Blue
$classic-secondary-color: #d9e1e8; // Pattens Blue
$classic-highlight-color: #2b90d9; // Summer Sky
// Variables for defaults in UI
$base-shadow-color: $black !default;
@ -21,10 +21,10 @@ $valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
// Tell UI to use selected colors
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
// Variables for texts
@ -48,7 +48,15 @@ $media-modal-media-max-width: 100%;
$media-modal-media-max-height: 80%;
$no-gap-breakpoint: 415px;
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
$column-breakpoint: 700px;
$small-breakpoint: 960px;
$xs-top-breakpoint: 738px;
$xs-breakpoint: 675px;
// pictures
$avatar-side: 55px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;

View File

@ -95,9 +95,9 @@
}
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
width: $avatar-side;
height: $avatar-side;
background-size: $avatar-side $avatar-side;
}
}
}
@ -537,3 +537,32 @@ $fluid-breakpoint: $maximum-width + 20px;
}
}
}
// christmas snow
.round-button {
cursor: pointer;
margin: 0.5em;
width: 2em;
height: 2em;
display: inline-block;
}
.snow-button {
-webkit-border-radius: 100%;
-moz-border-radius: 100%;
border-radius: 100%;
background: $ui-secondary-color;
@extend .round-button;
.icon {
margin: 0.5em;
}
&:hover {
background: $ui-primary-color;
}
&.active {
background: $ui-highlight-color;
}
}

View File

@ -22,3 +22,4 @@ $action-button-color: #8d9ac2;
$inverted-text-color: $black !default;
$lighter-text-color: darken($ui-base-color, 6%) !default;
$light-text-color: darken($ui-primary-color, 40%) !default;
$avatar-side: 55px;

View File

@ -1,30 +1,26 @@
@import 'large_center';
// custom sheet made on https://www.cipherbliss.com
// Commonly used web colors
$black: #fff; // Black
$white: #000; // White
$success-green: #6bbd77 !default; // Padua
$error-red: #d4839b !default; // Cerise
$warning-red: #528dc8 !default; // Sunset Orange
$gold-star: #00ec84 !default; // Dark Goldenrod
// User Interface Colors
$ui-base-color: #313644; // Midnight Express
$ui-base-lighter-color: #bdd2d6;
$ui-primary-color: #a1ccff; // Echo Blue
$ui-secondary-color: #7fc0ff; // Pattens Blue
$ui-highlight-color: #00a7d1; // Summer Sky
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
// fix
.media-gallery {
margin-left: 0;
}
// then we import the rest of the world
@import 'application';
@import 'bliss/messaging';
//@import 'large_center';
//// custom sheet made on https://www.cipherbliss.com
//// Commonly used web colors
//$black: #fff; // Black
//$white: #000; // White
//$success-green: #6bbd77 !default; // Padua
//$error-red: #d4839b !default; // Cerise
//$warning-red: #528dc8 !default; // Sunset Orange
//$gold-star: #00ec84 !default; // Dark Goldenrod
//
//// User Interface Colors
//$ui-base-color: #313644; // Midnight Express
//$ui-base-lighter-color: #bdd2d6;
//$ui-primary-color: #a1ccff; // Echo Blue
//$ui-secondary-color: #7fc0ff; // Pattens Blue
//$ui-highlight-color: #00a7d1; // Summer Sky
//$avatar-side: 55px;
//// Variables for components
//$media-modal-media-max-width: 100%;
//// put margins on top and bottom of image to avoid the screen covered by image.
//$media-modal-media-max-height: 80%;
//// fix
//
//// then we import the rest of the world
//@import 'application';
//@import 'bliss/messaging';

View File

@ -751,9 +751,9 @@ $small-breakpoint: 960px;
}
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
width: $avatar-side;
height: $avatar-side;
background-size: $avatar-side $avatar-side;
}
}

View File

@ -181,18 +181,39 @@ $content-width: 840px;
padding-top: 30px;
}
&-heading {
display: flex;
padding-bottom: 40px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
margin-bottom: 40px;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
&-actions {
display: inline-flex;
& > * {
margin-left: 5px;
}
}
@media screen and (max-width: $no-columns-breakpoint) {
border-bottom: 0;
padding-bottom: 0;
}
}
h2 {
color: $secondary-text-color;
font-size: 24px;
line-height: 28px;
font-weight: 400;
padding-bottom: 40px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
margin-bottom: 40px;
@media screen and (max-width: $no-columns-breakpoint) {
border-bottom: 0;
padding-bottom: 0;
font-weight: 700;
}
}

View File

@ -229,3 +229,7 @@ button {
}
}
}
.padded {
padding: 1em;
}

View File

@ -949,8 +949,12 @@
}
@keyframes fade {
0% { opacity: 0; }
100% { opacity: 1; }
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
opacity: 1;
@ -2175,7 +2179,7 @@ a.account__display-name {
.scrollable {
overflow: visible;
@supports(display: grid) {
@supports (display: grid) {
contain: content;
}
}
@ -2333,6 +2337,9 @@ a.account__display-name {
.columns-area__panels__pane--navigational {
display: none;
}
.getting-started__footer {
width: 250px;
}
}
@media screen and (min-width: 600px + (285px * 2) + (10px * 2)) {
@ -2512,13 +2519,12 @@ a.account__display-name {
overflow-x: hidden;
flex: 1 1 auto;
-webkit-overflow-scrolling: touch;
will-change: transform; // improves perf in mobile Chrome
&.optionally-scrollable {
overflow-y: auto;
}
@supports(display: grid) { // hack to fix Chrome <57
@supports (display: grid) { // hack to fix Chrome <57
contain: strict;
}
@ -2535,7 +2541,7 @@ a.account__display-name {
}
.scrollable.fullscreen {
@supports(display: grid) { // hack to fix Chrome <57
@supports (display: grid) { // hack to fix Chrome <57
contain: none;
}
}
@ -3465,9 +3471,15 @@ a.status-card.compact:hover {
}
@keyframes loader-label {
0% { opacity: 0.25; }
30% { opacity: 1; }
100% { opacity: 0.25; }
0% {
opacity: 0.25;
}
30% {
opacity: 1;
}
100% {
opacity: 0.25;
}
}
.video-error-cover {
@ -3756,7 +3768,7 @@ a.status-card.compact:hover {
align-items: center;
justify-content: center;
@supports(display: grid) { // hack to fix Chrome <57
@supports (display: grid) { // hack to fix Chrome <57
contain: strict;
}
@ -5161,6 +5173,7 @@ a.status-card.compact:hover {
overflow: hidden;
position: absolute;
}
/* End Media Gallery */
.detailed,
@ -5832,9 +5845,15 @@ noscript {
}
@keyframes flicker {
0% { opacity: 1; }
30% { opacity: 0.75; }
100% { opacity: 1; }
0% {
opacity: 1;
}
30% {
opacity: 0.75;
}
100% {
opacity: 1;
}
}
@media screen and (max-width: 630px) and (max-height: 400px) {
@ -5867,13 +5886,13 @@ noscript {
& > .icon-button.close {
will-change: opacity transform;
transition: opacity $duration * 0.5 $delay,
transform $duration $delay;
transform $duration $delay;
}
& > .compose__action-bar .icon-button {
will-change: opacity transform;
transition: opacity $duration * 0.5 $delay + $duration * 0.5,
transform $duration $delay;
transform $duration $delay;
}
}
}

View File

@ -21,7 +21,7 @@
justify-content: center;
align-items: center;
svg {
> svg {
fill: $primary-text-color;
height: 42px;
margin-right: 10px;

View File

@ -0,0 +1,3 @@
$messagingBoxWidth: 20em;
$messagingBoxHeight: 100%;
$conversationBoxHeight: 20em;

View File

@ -0,0 +1,16 @@
.contact-list-container {
overflow-x: auto;
}
.contact-list {
display: block;
padding: 1em;
min-height: 5em;
list-style-type: none;
color: white;
.contact {
margin-bottom: 0.5rem;
}
}

View File

@ -0,0 +1,16 @@
.fixed-box {
border: solid 1px white;
padding: 1em;
position: fixed;
bottom: 0;
}
.airmail-border {
border: 0.25em solid transparent;
border-image: 4 repeating-linear-gradient(-45deg, red 0, red 1em, white 0, white 2em,
#58a 0, #58a 3em, white 0, white 4em);
border-right-width: 0;
border-bottom-width: 0;
border-top-width: 0;
}

View File

@ -0,0 +1,6 @@
@import "config";
@import "helpers";
@import "messaging";
@import "contacts";
@import "messaging-conversation";
@import "messaging-stream";

View File

@ -0,0 +1,87 @@
.main-instant-messaging {
.conversation {
.conversation__content {
padding-right: 0;
}
.conversation_reply,
.icon-button {
&:hover {
color: $ui-highlight-color;
background: mix($ui-base-color, $ui-secondary-color);
}
}
.conversation_reply,
.icon-button,
.status__action-bar-dropdown {
display: inline-block;
float: right;
width: 18em;
height: 3.2em;
text-align: center;
}
}
.conversations_list {
@extend .fixed-box;
right: $messagingBoxWidth + 2em;
bottom: 0;
width: 100%;
padding: 0.5em;
overflow-x: auto;
border: 0;
}
.conversation-item {
float: right;
width: 20em;
margin-left: 2em;
padding: 1em;
border-radius: 15px;
border: 3px solid $classic-primary-color;
-moz-border-radius-bottomleft: 0;
-moz-border-radius-bottomright: 0;
background: $classic-base-color;
&.has-new-message {
background: $ui-highlight-color;
color: $classic-primary-color;
}
&.is-focused {
border-color: $ui-base-color;
}
.username {
font-weight: bold;
}
.conversation-stream,
.conversation_input {
height: 100%;
display: block;
}
&.hidden {
.conversation-stream,
.conversation_input {
display: none;
}
}
.btn {
background: $classic-base-color;
color: $ui-base-color;
float: right;
padding: 0 1em;
margin-left: 1em;
border: 0;
}
}
}

View File

@ -0,0 +1,55 @@
.conversation-stream {
padding-top: 1em;
height: 5em;
overflow: auto;
width: 100%;
.message {
clear: both;
}
.messages >
.message {
-webkit-border-radius: 0.5rem;
-moz-border-radius: 0.5rem;
border-radius: 0.5rem;
margin-bottom: 0.5em;
padding: 0.5em 1em;
display: block;
}
.ours {
text-align: right;
background: $light-text-color;
float: right;
.arrow-down {
border-top-color: $light-text-color;
left: 100%;
}
}
.theirs {
text-align: left;
background: $ui-highlight-color;
float: left;
.arrow-down {
border-top-color: $ui-highlight-color;
right: 1em;
}
}
.arrow-down {
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 20px solid transparent;
border-top: 20px solid $classic-primary-color;
position: relative;
bottom: -1em;
}
}

View File

@ -0,0 +1,54 @@
.status-direct,
.item-list .conversation {
@extend .airmail-border;
}
.main-instant-messaging {
.column-header__icon {
color: white;
}
}
.messaging-box {
@extend .fixed-box;
right: 1em;
width: $messagingBoxWidth;
background: $ui-base-color;
height: 4em;
color: white;
z-index: 10;
&.active {
height: $messagingBoxHeight;
}
.messager-textarea {
width: 100%;
}
.title {
color: white;
}
}
.toggle-list {
float: right;
margin-left: 1em;
background: $ui-primary-color;
border: 0;
padding: .5em;
width: 2em;
&.active {
background: $highlight-text-color;
}
}
.conversation_created-at {
margin-right: 1em;
}

View File

@ -1,18 +1,18 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$red-bookmark: $warning-red;
// Values from the classic Mastodon UI
$classic-base-color: #282c37; // Midnight Express
$classic-primary-color: #9baec8; // Echo Blue
$classic-secondary-color: #d9e1e8; // Pattens Blue
$classic-highlight-color: #2b90d9; // Summer Sky
$classic-base-color: #282c37; // Midnight Express
$classic-primary-color: #9baec8; // Echo Blue
$classic-secondary-color: #d9e1e8; // Pattens Blue
$classic-highlight-color: #2b90d9; // Summer Sky
// Variables for defaults in UI
$base-shadow-color: $black !default;
@ -23,10 +23,10 @@ $valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
// Tell UI to use selected colors
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
// Variables for texts
@ -76,3 +76,5 @@ $no-gap-breakpoint: 415px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;
$avatar-side: 55px;

View File

@ -372,8 +372,8 @@
.account__avatar {
flex: 0 0 auto;
width: 36px;
height: 36px;
width: $avatar-side;
height: $avatar-side;
border-radius: 50%;
position: relative;
margin-left: -10px;

View File

@ -5,7 +5,7 @@ class ActivityPub::Activity
include Redisable
SUPPORTED_TYPES = %w(Note Question).freeze
CONVERTED_TYPES = %w(Image Audio Video Article Page).freeze
CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze
def initialize(json, account, **options)
@json = json

View File

@ -157,7 +157,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return if tag['name'].blank?
Tag.find_or_create_by_names(tag['name']) do |hashtag|
@tags << hashtag unless @tags.include?(hashtag)
@tags << hashtag unless @tags.include?(hashtag) || !hashtag.valid?
end
rescue ActiveRecord::RecordInvalid
nil
@ -167,7 +167,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return if tag['href'].blank?
account = account_from_uri(tag['href'])
account = ::FetchRemoteAccountService.new.call(tag['href']) if account.nil?
account = ActivityPub::FetchRemoteAccountService.new.call(tag['href']) if account.nil?
return if account.nil?

View File

@ -7,7 +7,7 @@
# user_id :bigint(8)
# dump_file_name :string
# dump_content_type :string
# dump_file_size :integer
# dump_file_size :bigint
# dump_updated_at :datetime
# processed :boolean default(FALSE), not null
# created_at :datetime not null

View File

@ -16,6 +16,7 @@ class Form::AdminSettings
open_deletion
timeline_preview
show_staff_badge
enable_bootstrap_timeline_accounts
bootstrap_timeline_accounts
theme
min_invite_role
@ -40,6 +41,7 @@ class Form::AdminSettings
open_deletion
timeline_preview
show_staff_badge
enable_bootstrap_timeline_accounts
activity_api_enabled
peers_api_enabled
show_known_fediverse_at_about_page

View File

@ -40,7 +40,7 @@ class Form::CustomEmojiBatch
if category_id.present?
CustomEmojiCategory.find(category_id)
elsif category_name.present?
CustomEmojiCategory.create!(name: category_name)
CustomEmojiCategory.find_or_create_by!(name: category_name)
end
end

View File

@ -117,7 +117,7 @@ class Tag < ApplicationRecord
class << self
def find_or_create_by_names(name_or_names)
Array(name_or_names).map(&method(:normalize)).uniq { |str| str.mb_chars.downcase.to_s }.map do |normalized_name|
tag = matching_name(normalized_name).first || create!(name: normalized_name)
tag = matching_name(normalized_name).first || create(name: normalized_name)
yield tag if block_given?

View File

@ -3,6 +3,8 @@
require 'rubygems/package'
class BackupService < BaseService
include Payloadable
attr_reader :account, :backup, :collection
def call(backup)
@ -20,7 +22,7 @@ class BackupService < BaseService
account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
statuses.each do |status|
item = serialize(status, ActivityPub::ActivitySerializer)
item = serialize_payload(status, ActivityPub::ActivitySerializer, signer: @account)
item.delete(:'@context')
unless item[:type] == 'Announce' || item[:object][:attachment].blank?

View File

@ -5,7 +5,7 @@ class BootstrapTimelineService < BaseService
@source_account = source_account
autofollow_inviter!
autofollow_bootstrap_timeline_accounts!
autofollow_bootstrap_timeline_accounts! if Setting.enable_bootstrap_timeline_accounts
end
private

View File

@ -45,7 +45,7 @@ class FetchLinkCardService < BaseService
def html
return @html if defined?(@html)
Request.new(:get, @url).perform do |res|
Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
if res.code == 200 && res.mime_type == 'text/html'
@html = res.body_with_limit
@html_charset = res.charset

View File

@ -93,7 +93,7 @@ class FetchOEmbedService
def html
return @html if defined?(@html)
@html = @options[:html] || Request.new(:get, @url).perform do |res|
@html = @options[:html] || Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
res.code != 200 || res.mime_type != 'text/html' ? nil : res.body_with_limit
end
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
class FetchRemoteAccountService < BaseService
def call(url, prefetched_body = nil, protocol = :ostatus)
if prefetched_body.nil?
resource_url, resource_options, protocol = FetchResourceService.new.call(url)
else
resource_url = url
resource_options = { prefetched_body: prefetched_body }
end
case protocol
when :activitypub
ActivityPub::FetchRemoteAccountService.new.call(resource_url, **resource_options)
end
end
end

View File

@ -1,17 +1,14 @@
# frozen_string_literal: true
class FetchRemoteStatusService < BaseService
def call(url, prefetched_body = nil, protocol = :ostatus)
def call(url, prefetched_body = nil)
if prefetched_body.nil?
resource_url, resource_options, protocol = FetchResourceService.new.call(url)
resource_url, resource_options = FetchResourceService.new.call(url)
else
resource_url = url
resource_options = { prefetched_body: prefetched_body }
end
case protocol
when :activitypub
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options)
end
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options) unless resource_url.nil?
end
end

View File

@ -33,7 +33,7 @@ class FetchResourceService < BaseService
body = response.body_with_limit
json = body_to_json(body)
[json['id'], { prefetched_body: body, id: true }, :activitypub] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
[json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
elsif !terminal
link_header = response['Link'] && parse_link_header(response)

View File

@ -19,9 +19,9 @@ class ResolveURLService < BaseService
def process_url
if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
FetchRemoteAccountService.new.call(resource_url, body, protocol)
ActivityPub::FetchRemoteAccountService.new.call(resource_url, prefetched_body: body)
elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
status = FetchRemoteStatusService.new.call(resource_url, body, protocol)
status = FetchRemoteStatusService.new.call(resource_url, body)
authorize_with @on_behalf_of, status, :show? unless status.nil?
status
elsif fetched_resource.nil? && @on_behalf_of.present?
@ -45,12 +45,8 @@ class ResolveURLService < BaseService
fetched_resource.second[:prefetched_body]
end
def protocol
fetched_resource.third
end
def type
return json_data['type'] if protocol == :activitypub
json_data['type']
end
def json_data

View File

@ -4,21 +4,12 @@
- content_for :page_title do
= t('admin.reports.report', id: @report.id)
%div{ style: 'overflow: hidden; margin-bottom: 20px' }
- content_for :page_heading_actions do
- if @report.unresolved?
%div{ style: 'float: right' }
- if @report.target_account.local?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button'
= link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, type: 'suspend', report_id: @report.id), class: 'button button--destructive'
%div{ style: 'float: left' }
= link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button'
= link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button'
- else
= link_to t('admin.reports.mark_as_unresolved'), reopen_admin_report_path(@report), method: :post, class: 'button'
%hr.spacer
.table-wrapper
%table.table.inline-table
%tbody
@ -77,6 +68,17 @@
%hr.spacer
%div{ style: 'overflow: hidden; margin-bottom: 20px; clear: both' }
- if @report.unresolved?
%div{ style: 'float: right' }
- if @report.target_account.local?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button'
= link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, type: 'suspend', report_id: @report.id), class: 'button button--destructive'
%hr.spacer
.speech-bubble
.speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
.speech-bubble__owner

View File

@ -1,3 +1,6 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.settings.title')
@ -38,7 +41,9 @@
%hr.spacer/
.fields-group
= f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html')
= f.input :enable_bootstrap_timeline_accounts, as: :boolean, wrapper: :with_label, label: t('admin.settings.enable_bootstrap_timeline_accounts.title')
.fields-group
= f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html'), disabled: !Setting.enable_bootstrap_timeline_accounts
%hr.spacer/

View File

@ -21,7 +21,12 @@
.content-wrapper
.content
%h2= yield :page_title
.content-heading
%h2= yield :page_title
- if :page_heading_actions
.content-heading-actions
= yield :page_heading_actions
= render 'application/flashes'

View File

@ -9,11 +9,11 @@
%td= number_to_human_size @export.total_storage
%td
%tr
%th= t('accounts.posts', count: @export.total_statuses)
%th= t('accounts.posts_tab_heading')
%td= number_with_delimiter @export.total_statuses
%td
%tr
%th= t('exports.follows')
%th= t('admin.accounts.follows')
%td= number_with_delimiter @export.total_follows
%td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
%tr
@ -21,7 +21,7 @@
%td= number_with_delimiter @export.total_lists
%td= table_link_to 'download', t('exports.csv'), settings_exports_lists_path(format: :csv)
%tr
%th= t('accounts.followers', count: @export.total_followers)
%th= t('admin.accounts.followers')
%td= number_with_delimiter @export.total_followers
%td
%tr

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