diff options
author | beatrix <beatrix.bitrot@gmail.com> | 2017-12-06 17:44:07 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-12-06 17:44:07 -0500 |
commit | 81b01457598459c42a7b14d9aa14f91ba60dcae1 (patch) | |
tree | 7d3e6dadb75f3be95e5a5ed8b7ecfe90e7711831 /app/javascript/themes/glitch | |
parent | f1cbea77a4a52929244198dcbde26d63d837489a (diff) | |
parent | 017fc81caf8f265e5c5543186877437485625795 (diff) |
Merge pull request #229 from glitch-soc/glitch-theme
Advanced Next-Level Flavours And Skins For Mastodon™
Diffstat (limited to 'app/javascript/themes/glitch')
291 files changed, 0 insertions, 30556 deletions
diff --git a/app/javascript/themes/glitch/actions/accounts.js b/app/javascript/themes/glitch/actions/accounts.js deleted file mode 100644 index f1a8c5471..000000000 --- a/app/javascript/themes/glitch/actions/accounts.js +++ /dev/null @@ -1,661 +0,0 @@ -import api, { getLinks } from 'themes/glitch/util/api'; - -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_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST'; -export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS'; -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_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST'; -export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS'; -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_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; -export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS'; -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 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_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST'; -export const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS'; -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_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST'; -export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS'; -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 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_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_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_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 function fetchAccount(id) { - return (dispatch, getState) => { - dispatch(fetchRelationships([id])); - - if (getState().getIn(['accounts', id], null) !== null) { - return; - } - - dispatch(fetchAccountRequest(id)); - - api(getState).get(`/api/v1/accounts/${id}`).then(response => { - dispatch(fetchAccountSuccess(response.data)); - }).catch(error => { - dispatch(fetchAccountFail(id, error)); - }); - }; -}; - -export function fetchAccountRequest(id) { - return { - type: ACCOUNT_FETCH_REQUEST, - id, - }; -}; - -export function fetchAccountSuccess(account) { - return { - type: ACCOUNT_FETCH_SUCCESS, - account, - }; -}; - -export function fetchAccountFail(id, error) { - return { - type: ACCOUNT_FETCH_FAIL, - id, - error, - skipAlert: true, - }; -}; - -export function followAccount(id, reblogs = true) { - return (dispatch, getState) => { - const alreadyFollowing = getState().getIn(['relationships', id, 'following']); - dispatch(followAccountRequest(id)); - - api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { - dispatch(followAccountSuccess(response.data, alreadyFollowing)); - }).catch(error => { - dispatch(followAccountFail(error)); - }); - }; -}; - -export function unfollowAccount(id) { - return (dispatch, getState) => { - dispatch(unfollowAccountRequest(id)); - - api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => { - dispatch(unfollowAccountSuccess(response.data, getState().get('statuses'))); - }).catch(error => { - dispatch(unfollowAccountFail(error)); - }); - }; -}; - -export function followAccountRequest(id) { - return { - type: ACCOUNT_FOLLOW_REQUEST, - id, - }; -}; - -export function followAccountSuccess(relationship, alreadyFollowing) { - return { - type: ACCOUNT_FOLLOW_SUCCESS, - relationship, - alreadyFollowing, - }; -}; - -export function followAccountFail(error) { - return { - type: ACCOUNT_FOLLOW_FAIL, - error, - }; -}; - -export function unfollowAccountRequest(id) { - return { - type: ACCOUNT_UNFOLLOW_REQUEST, - id, - }; -}; - -export function unfollowAccountSuccess(relationship, statuses) { - return { - type: ACCOUNT_UNFOLLOW_SUCCESS, - relationship, - statuses, - }; -}; - -export function unfollowAccountFail(error) { - return { - type: ACCOUNT_UNFOLLOW_FAIL, - error, - }; -}; - -export function blockAccount(id) { - return (dispatch, getState) => { - dispatch(blockAccountRequest(id)); - - api(getState).post(`/api/v1/accounts/${id}/block`).then(response => { - // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - dispatch(blockAccountSuccess(response.data, getState().get('statuses'))); - }).catch(error => { - dispatch(blockAccountFail(id, error)); - }); - }; -}; - -export function unblockAccount(id) { - return (dispatch, getState) => { - dispatch(unblockAccountRequest(id)); - - api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => { - dispatch(unblockAccountSuccess(response.data)); - }).catch(error => { - dispatch(unblockAccountFail(id, error)); - }); - }; -}; - -export function blockAccountRequest(id) { - return { - type: ACCOUNT_BLOCK_REQUEST, - id, - }; -}; - -export function blockAccountSuccess(relationship, statuses) { - return { - type: ACCOUNT_BLOCK_SUCCESS, - 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) => { - dispatch(muteAccountRequest(id)); - - api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => { - // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); - }).catch(error => { - dispatch(muteAccountFail(id, error)); - }); - }; -}; - -export function unmuteAccount(id) { - return (dispatch, getState) => { - dispatch(unmuteAccountRequest(id)); - - api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => { - dispatch(unmuteAccountSuccess(response.data)); - }).catch(error => { - dispatch(unmuteAccountFail(id, error)); - }); - }; -}; - -export function muteAccountRequest(id) { - return { - type: ACCOUNT_MUTE_REQUEST, - id, - }; -}; - -export function muteAccountSuccess(relationship, statuses) { - return { - type: ACCOUNT_MUTE_SUCCESS, - 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) => { - dispatch(fetchFollowersRequest(id)); - - api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => { - dispatch(fetchFollowersFail(id, error)); - }); - }; -}; - -export function fetchFollowersRequest(id) { - return { - type: FOLLOWERS_FETCH_REQUEST, - id, - }; -}; - -export function fetchFollowersSuccess(id, accounts, next) { - return { - type: FOLLOWERS_FETCH_SUCCESS, - id, - accounts, - next, - }; -}; - -export function fetchFollowersFail(id, error) { - return { - type: FOLLOWERS_FETCH_FAIL, - id, - error, - }; -}; - -export function expandFollowers(id) { - return (dispatch, getState) => { - const url = getState().getIn(['user_lists', 'followers', id, 'next']); - - if (url === null) { - return; - } - - dispatch(expandFollowersRequest(id)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => { - dispatch(expandFollowersFail(id, error)); - }); - }; -}; - -export function expandFollowersRequest(id) { - return { - type: FOLLOWERS_EXPAND_REQUEST, - id, - }; -}; - -export function expandFollowersSuccess(id, accounts, next) { - return { - type: FOLLOWERS_EXPAND_SUCCESS, - id, - accounts, - next, - }; -}; - -export function expandFollowersFail(id, error) { - return { - type: FOLLOWERS_EXPAND_FAIL, - id, - error, - }; -}; - -export function fetchFollowing(id) { - return (dispatch, getState) => { - dispatch(fetchFollowingRequest(id)); - - api(getState).get(`/api/v1/accounts/${id}/following`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => { - dispatch(fetchFollowingFail(id, error)); - }); - }; -}; - -export function fetchFollowingRequest(id) { - return { - type: FOLLOWING_FETCH_REQUEST, - id, - }; -}; - -export function fetchFollowingSuccess(id, accounts, next) { - return { - type: FOLLOWING_FETCH_SUCCESS, - id, - accounts, - next, - }; -}; - -export function fetchFollowingFail(id, error) { - return { - type: FOLLOWING_FETCH_FAIL, - id, - error, - }; -}; - -export function expandFollowing(id) { - return (dispatch, getState) => { - const url = getState().getIn(['user_lists', 'following', id, 'next']); - - if (url === null) { - return; - } - - dispatch(expandFollowingRequest(id)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => { - dispatch(expandFollowingFail(id, error)); - }); - }; -}; - -export function expandFollowingRequest(id) { - return { - type: FOLLOWING_EXPAND_REQUEST, - id, - }; -}; - -export function expandFollowingSuccess(id, accounts, next) { - return { - type: FOLLOWING_EXPAND_SUCCESS, - id, - accounts, - next, - }; -}; - -export function expandFollowingFail(id, error) { - return { - type: FOLLOWING_EXPAND_FAIL, - id, - error, - }; -}; - -export function fetchRelationships(accountIds) { - return (dispatch, getState) => { - const loadedRelationships = getState().get('relationships'); - const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null); - - if (newAccountIds.length === 0) { - return; - } - - dispatch(fetchRelationshipsRequest(newAccountIds)); - - api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { - dispatch(fetchRelationshipsSuccess(response.data)); - }).catch(error => { - dispatch(fetchRelationshipsFail(error)); - }); - }; -}; - -export function fetchRelationshipsRequest(ids) { - return { - type: RELATIONSHIPS_FETCH_REQUEST, - ids, - skipLoading: true, - }; -}; - -export function fetchRelationshipsSuccess(relationships) { - return { - type: RELATIONSHIPS_FETCH_SUCCESS, - relationships, - skipLoading: true, - }; -}; - -export function fetchRelationshipsFail(error) { - return { - type: RELATIONSHIPS_FETCH_FAIL, - error, - skipLoading: true, - }; -}; - -export function fetchFollowRequests() { - return (dispatch, getState) => { - dispatch(fetchFollowRequestsRequest()); - - api(getState).get('/api/v1/follow_requests').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - 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 { - type: FOLLOW_REQUESTS_FETCH_SUCCESS, - accounts, - next, - }; -}; - -export function fetchFollowRequestsFail(error) { - return { - type: FOLLOW_REQUESTS_FETCH_FAIL, - error, - }; -}; - -export function expandFollowRequests() { - return (dispatch, getState) => { - const url = getState().getIn(['user_lists', 'follow_requests', 'next']); - - if (url === null) { - return; - } - - dispatch(expandFollowRequestsRequest()); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - 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 { - type: FOLLOW_REQUESTS_EXPAND_SUCCESS, - accounts, - next, - }; -}; - -export function expandFollowRequestsFail(error) { - return { - type: FOLLOW_REQUESTS_EXPAND_FAIL, - error, - }; -}; - -export function authorizeFollowRequest(id) { - return (dispatch, getState) => { - dispatch(authorizeFollowRequestRequest(id)); - - api(getState) - .post(`/api/v1/follow_requests/${id}/authorize`) - .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 { - type: FOLLOW_REQUEST_AUTHORIZE_FAIL, - id, - error, - }; -}; - - -export function rejectFollowRequest(id) { - return (dispatch, getState) => { - dispatch(rejectFollowRequestRequest(id)); - - api(getState) - .post(`/api/v1/follow_requests/${id}/reject`) - .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 { - type: FOLLOW_REQUEST_REJECT_FAIL, - id, - error, - }; -}; diff --git a/app/javascript/themes/glitch/actions/alerts.js b/app/javascript/themes/glitch/actions/alerts.js deleted file mode 100644 index f37fdeeb6..000000000 --- a/app/javascript/themes/glitch/actions/alerts.js +++ /dev/null @@ -1,24 +0,0 @@ -export const ALERT_SHOW = 'ALERT_SHOW'; -export const ALERT_DISMISS = 'ALERT_DISMISS'; -export const ALERT_CLEAR = 'ALERT_CLEAR'; - -export function dismissAlert(alert) { - return { - type: ALERT_DISMISS, - alert, - }; -}; - -export function clearAlert() { - return { - type: ALERT_CLEAR, - }; -}; - -export function showAlert(title, message) { - return { - type: ALERT_SHOW, - title, - message, - }; -}; diff --git a/app/javascript/themes/glitch/actions/blocks.js b/app/javascript/themes/glitch/actions/blocks.js deleted file mode 100644 index 6ba9460f0..000000000 --- a/app/javascript/themes/glitch/actions/blocks.js +++ /dev/null @@ -1,82 +0,0 @@ -import api, { getLinks } from 'themes/glitch/util/api'; -import { fetchRelationships } from './accounts'; - -export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; -export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; -export const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL'; - -export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; -export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; -export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; - -export function fetchBlocks() { - return (dispatch, getState) => { - dispatch(fetchBlocksRequest()); - - api(getState).get('/api/v1/blocks').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(fetchBlocksFail(error))); - }; -}; - -export function fetchBlocksRequest() { - return { - type: BLOCKS_FETCH_REQUEST, - }; -}; - -export function fetchBlocksSuccess(accounts, next) { - return { - type: BLOCKS_FETCH_SUCCESS, - accounts, - next, - }; -}; - -export function fetchBlocksFail(error) { - return { - type: BLOCKS_FETCH_FAIL, - error, - }; -}; - -export function expandBlocks() { - return (dispatch, getState) => { - const url = getState().getIn(['user_lists', 'blocks', 'next']); - - if (url === null) { - return; - } - - dispatch(expandBlocksRequest()); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(expandBlocksFail(error))); - }; -}; - -export function expandBlocksRequest() { - return { - type: BLOCKS_EXPAND_REQUEST, - }; -}; - -export function expandBlocksSuccess(accounts, next) { - return { - type: BLOCKS_EXPAND_SUCCESS, - accounts, - next, - }; -}; - -export function expandBlocksFail(error) { - return { - type: BLOCKS_EXPAND_FAIL, - error, - }; -}; diff --git a/app/javascript/themes/glitch/actions/bundles.js b/app/javascript/themes/glitch/actions/bundles.js deleted file mode 100644 index ecc9c8f7d..000000000 --- a/app/javascript/themes/glitch/actions/bundles.js +++ /dev/null @@ -1,25 +0,0 @@ -export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST'; -export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS'; -export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL'; - -export function fetchBundleRequest(skipLoading) { - return { - type: BUNDLE_FETCH_REQUEST, - skipLoading, - }; -} - -export function fetchBundleSuccess(skipLoading) { - return { - type: BUNDLE_FETCH_SUCCESS, - skipLoading, - }; -} - -export function fetchBundleFail(error, skipLoading) { - return { - type: BUNDLE_FETCH_FAIL, - error, - skipLoading, - }; -} diff --git a/app/javascript/themes/glitch/actions/cards.js b/app/javascript/themes/glitch/actions/cards.js deleted file mode 100644 index 2a1bc369a..000000000 --- a/app/javascript/themes/glitch/actions/cards.js +++ /dev/null @@ -1,52 +0,0 @@ -import api from 'themes/glitch/util/api'; - -export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST'; -export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS'; -export const STATUS_CARD_FETCH_FAIL = 'STATUS_CARD_FETCH_FAIL'; - -export function fetchStatusCard(id) { - return (dispatch, getState) => { - if (getState().getIn(['cards', id], null) !== null) { - return; - } - - dispatch(fetchStatusCardRequest(id)); - - api(getState).get(`/api/v1/statuses/${id}/card`).then(response => { - if (!response.data.url) { - return; - } - - dispatch(fetchStatusCardSuccess(id, response.data)); - }).catch(error => { - dispatch(fetchStatusCardFail(id, error)); - }); - }; -}; - -export function fetchStatusCardRequest(id) { - return { - type: STATUS_CARD_FETCH_REQUEST, - id, - skipLoading: true, - }; -}; - -export function fetchStatusCardSuccess(id, card) { - return { - type: STATUS_CARD_FETCH_SUCCESS, - id, - card, - skipLoading: true, - }; -}; - -export function fetchStatusCardFail(id, error) { - return { - type: STATUS_CARD_FETCH_FAIL, - id, - error, - skipLoading: true, - skipAlert: true, - }; -}; diff --git a/app/javascript/themes/glitch/actions/columns.js b/app/javascript/themes/glitch/actions/columns.js deleted file mode 100644 index bcb0cdf98..000000000 --- a/app/javascript/themes/glitch/actions/columns.js +++ /dev/null @@ -1,40 +0,0 @@ -import { saveSettings } from './settings'; - -export const COLUMN_ADD = 'COLUMN_ADD'; -export const COLUMN_REMOVE = 'COLUMN_REMOVE'; -export const COLUMN_MOVE = 'COLUMN_MOVE'; - -export function addColumn(id, params) { - return dispatch => { - dispatch({ - type: COLUMN_ADD, - id, - params, - }); - - dispatch(saveSettings()); - }; -}; - -export function removeColumn(uuid) { - return dispatch => { - dispatch({ - type: COLUMN_REMOVE, - uuid, - }); - - dispatch(saveSettings()); - }; -}; - -export function moveColumn(uuid, direction) { - return dispatch => { - dispatch({ - type: COLUMN_MOVE, - uuid, - direction, - }); - - dispatch(saveSettings()); - }; -}; diff --git a/app/javascript/themes/glitch/actions/compose.js b/app/javascript/themes/glitch/actions/compose.js deleted file mode 100644 index 07c469477..000000000 --- a/app/javascript/themes/glitch/actions/compose.js +++ /dev/null @@ -1,398 +0,0 @@ -import api from 'themes/glitch/util/api'; -import { throttle } from 'lodash'; -import { search as emojiSearch } from 'themes/glitch/util/emoji/emoji_mart_search_light'; -import { useEmoji } from './emojis'; - -import { - updateTimeline, - refreshHomeTimeline, - refreshCommunityTimeline, - refreshPublicTimeline, - refreshDirectTimeline, -} from './timelines'; - -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_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_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; -export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; -export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; - -export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; -export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; - -export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE'; -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_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_DOODLE_SET = 'COMPOSE_DOODLE_SET'; - -export function changeCompose(text) { - return { - type: COMPOSE_CHANGE, - text: text, - }; -}; - -export function replyCompose(status, router) { - return (dispatch, getState) => { - dispatch({ - type: COMPOSE_REPLY, - status: status, - }); - - if (!getState().getIn(['compose', 'mounted'])) { - router.push('/statuses/new'); - } - }; -}; - -export function cancelReplyCompose() { - return { - type: COMPOSE_REPLY_CANCEL, - }; -}; - -export function resetCompose() { - return { - type: COMPOSE_RESET, - }; -}; - -export function mentionCompose(account, router) { - return (dispatch, getState) => { - dispatch({ - type: COMPOSE_MENTION, - account: account, - }); - - if (!getState().getIn(['compose', 'mounted'])) { - router.push('/statuses/new'); - } - }; -}; - -export function submitCompose() { - return function (dispatch, getState) { - let status = getState().getIn(['compose', 'text'], ''); - - if (!status || !status.length) { - return; - } - - dispatch(submitComposeRequest()); - if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) { - status = status + ' 👁️'; - } - api(getState).post('/api/v1/statuses', { - status, - in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), - media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')), - sensitive: getState().getIn(['compose', 'sensitive']), - spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), - visibility: getState().getIn(['compose', 'privacy']), - }, { - headers: { - 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), - }, - }).then(function (response) { - dispatch(submitComposeSuccess({ ...response.data })); - - // To make the app more responsive, immediately get the status into the columns - - const insertOrRefresh = (timelineId, refreshAction) => { - if (getState().getIn(['timelines', timelineId, 'online'])) { - dispatch(updateTimeline(timelineId, { ...response.data })); - } else if (getState().getIn(['timelines', timelineId, 'loaded'])) { - dispatch(refreshAction()); - } - }; - - insertOrRefresh('home', refreshHomeTimeline); - - if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { - insertOrRefresh('community', refreshCommunityTimeline); - insertOrRefresh('public', refreshPublicTimeline); - } else if (response.data.visibility === 'direct') { - insertOrRefresh('direct', refreshDirectTimeline); - } - }).catch(function (error) { - dispatch(submitComposeFail(error)); - }); - }; -}; - -export function submitComposeRequest() { - return { - type: COMPOSE_SUBMIT_REQUEST, - }; -}; - -export function submitComposeSuccess(status) { - return { - type: COMPOSE_SUBMIT_SUCCESS, - status: status, - }; -}; - -export function submitComposeFail(error) { - return { - type: COMPOSE_SUBMIT_FAIL, - error: error, - }; -}; - -export function doodleSet(options) { - return { - type: COMPOSE_DOODLE_SET, - options: options, - }; -}; - -export function uploadCompose(files) { - return function (dispatch, getState) { - if (getState().getIn(['compose', 'media_attachments']).size > 3) { - return; - } - - dispatch(uploadComposeRequest()); - - let data = new FormData(); - data.append('file', files[0]); - - api(getState).post('/api/v1/media', data, { - onUploadProgress: function (e) { - dispatch(uploadComposeProgress(e.loaded, e.total)); - }, - }).then(function (response) { - dispatch(uploadComposeSuccess(response.data)); - }).catch(function (error) { - dispatch(uploadComposeFail(error)); - }); - }; -}; - -export function changeUploadCompose(id, description) { - return (dispatch, getState) => { - dispatch(changeUploadComposeRequest()); - - api(getState).put(`/api/v1/media/${id}`, { description }).then(response => { - dispatch(changeUploadComposeSuccess(response.data)); - }).catch(error => { - dispatch(changeUploadComposeFail(id, error)); - }); - }; -}; - -export function changeUploadComposeRequest() { - return { - type: COMPOSE_UPLOAD_CHANGE_REQUEST, - skipLoading: true, - }; -}; -export function changeUploadComposeSuccess(media) { - return { - type: COMPOSE_UPLOAD_CHANGE_SUCCESS, - media: media, - skipLoading: true, - }; -}; - -export function changeUploadComposeFail(error) { - return { - type: COMPOSE_UPLOAD_CHANGE_FAIL, - error: error, - skipLoading: true, - }; -}; - -export function uploadComposeRequest() { - return { - type: COMPOSE_UPLOAD_REQUEST, - skipLoading: true, - }; -}; - -export function uploadComposeProgress(loaded, total) { - return { - type: COMPOSE_UPLOAD_PROGRESS, - loaded: loaded, - total: total, - }; -}; - -export function uploadComposeSuccess(media) { - return { - type: COMPOSE_UPLOAD_SUCCESS, - media: media, - skipLoading: true, - }; -}; - -export function uploadComposeFail(error) { - return { - type: COMPOSE_UPLOAD_FAIL, - error: error, - skipLoading: true, - }; -}; - -export function undoUploadCompose(media_id) { - return { - type: COMPOSE_UPLOAD_UNDO, - media_id: media_id, - }; -}; - -export function clearComposeSuggestions() { - return { - type: COMPOSE_SUGGESTIONS_CLEAR, - }; -}; - -const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => { - api(getState).get('/api/v1/accounts/search', { - params: { - q: token.slice(1), - resolve: false, - limit: 4, - }, - }).then(response => { - dispatch(readyComposeSuggestionsAccounts(token, response.data)); - }); -}, 200, { leading: true, trailing: true }); - -const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { - const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }); - dispatch(readyComposeSuggestionsEmojis(token, results)); -}; - -export function fetchComposeSuggestions(token) { - return (dispatch, getState) => { - if (token[0] === ':') { - fetchComposeSuggestionsEmojis(dispatch, getState, token); - } else { - fetchComposeSuggestionsAccounts(dispatch, getState, token); - } - }; -}; - -export function readyComposeSuggestionsEmojis(token, emojis) { - return { - type: COMPOSE_SUGGESTIONS_READY, - token, - emojis, - }; -}; - -export function readyComposeSuggestionsAccounts(token, accounts) { - return { - type: COMPOSE_SUGGESTIONS_READY, - token, - accounts, - }; -}; - -export function selectComposeSuggestion(position, token, suggestion) { - return (dispatch, getState) => { - let completion, startPosition; - - if (typeof suggestion === 'object' && suggestion.id) { - completion = suggestion.native || suggestion.colons; - startPosition = position - 1; - - dispatch(useEmoji(suggestion)); - } else { - completion = getState().getIn(['accounts', suggestion, 'acct']); - startPosition = position; - } - - dispatch({ - type: COMPOSE_SUGGESTION_SELECT, - position: startPosition, - token, - completion, - }); - }; -}; - -export function mountCompose() { - return { - type: COMPOSE_MOUNT, - }; -}; - -export function unmountCompose() { - return { - type: COMPOSE_UNMOUNT, - }; -}; - -export function toggleComposeAdvancedOption(option) { - return { - type: COMPOSE_ADVANCED_OPTIONS_CHANGE, - option: option, - }; -} - -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) { - return { - type: COMPOSE_EMOJI_INSERT, - position, - emoji, - }; -}; - -export function changeComposing(value) { - return { - type: COMPOSE_COMPOSING_CHANGE, - value, - }; -} diff --git a/app/javascript/themes/glitch/actions/domain_blocks.js b/app/javascript/themes/glitch/actions/domain_blocks.js deleted file mode 100644 index 0a880394a..000000000 --- a/app/javascript/themes/glitch/actions/domain_blocks.js +++ /dev/null @@ -1,117 +0,0 @@ -import api, { getLinks } from 'themes/glitch/util/api'; - -export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; -export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; -export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; - -export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; -export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS'; -export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; - -export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; -export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS'; -export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL'; - -export function blockDomain(domain, accountId) { - return (dispatch, getState) => { - dispatch(blockDomainRequest(domain)); - - api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { - dispatch(blockDomainSuccess(domain, accountId)); - }).catch(err => { - dispatch(blockDomainFail(domain, err)); - }); - }; -}; - -export function blockDomainRequest(domain) { - return { - type: DOMAIN_BLOCK_REQUEST, - domain, - }; -}; - -export function blockDomainSuccess(domain, accountId) { - return { - type: DOMAIN_BLOCK_SUCCESS, - domain, - accountId, - }; -}; - -export function blockDomainFail(domain, error) { - return { - type: DOMAIN_BLOCK_FAIL, - domain, - error, - }; -}; - -export function unblockDomain(domain, accountId) { - return (dispatch, getState) => { - dispatch(unblockDomainRequest(domain)); - - api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { - dispatch(unblockDomainSuccess(domain, accountId)); - }).catch(err => { - dispatch(unblockDomainFail(domain, err)); - }); - }; -}; - -export function unblockDomainRequest(domain) { - return { - type: DOMAIN_UNBLOCK_REQUEST, - domain, - }; -}; - -export function unblockDomainSuccess(domain, accountId) { - return { - type: DOMAIN_UNBLOCK_SUCCESS, - domain, - accountId, - }; -}; - -export function unblockDomainFail(domain, error) { - return { - type: DOMAIN_UNBLOCK_FAIL, - domain, - error, - }; -}; - -export function fetchDomainBlocks() { - return (dispatch, getState) => { - dispatch(fetchDomainBlocksRequest()); - - api(getState).get().then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null)); - }).catch(err => { - dispatch(fetchDomainBlocksFail(err)); - }); - }; -}; - -export function fetchDomainBlocksRequest() { - return { - type: DOMAIN_BLOCKS_FETCH_REQUEST, - }; -}; - -export function fetchDomainBlocksSuccess(domains, next) { - return { - type: DOMAIN_BLOCKS_FETCH_SUCCESS, - domains, - next, - }; -}; - -export function fetchDomainBlocksFail(error) { - return { - type: DOMAIN_BLOCKS_FETCH_FAIL, - error, - }; -}; diff --git a/app/javascript/themes/glitch/actions/emojis.js b/app/javascript/themes/glitch/actions/emojis.js deleted file mode 100644 index 7cd9d4b7b..000000000 --- a/app/javascript/themes/glitch/actions/emojis.js +++ /dev/null @@ -1,14 +0,0 @@ -import { saveSettings } from './settings'; - -export const EMOJI_USE = 'EMOJI_USE'; - -export function useEmoji(emoji) { - return dispatch => { - dispatch({ - type: EMOJI_USE, - emoji, - }); - - dispatch(saveSettings()); - }; -}; diff --git a/app/javascript/themes/glitch/actions/favourites.js b/app/javascript/themes/glitch/actions/favourites.js deleted file mode 100644 index e9b3559af..000000000 --- a/app/javascript/themes/glitch/actions/favourites.js +++ /dev/null @@ -1,83 +0,0 @@ -import api, { getLinks } from 'themes/glitch/util/api'; - -export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; -export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; -export const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL'; - -export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST'; -export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS'; -export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL'; - -export function fetchFavouritedStatuses() { - return (dispatch, getState) => { - dispatch(fetchFavouritedStatusesRequest()); - - api(getState).get('/api/v1/favourites').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(fetchFavouritedStatusesFail(error)); - }); - }; -}; - -export function fetchFavouritedStatusesRequest() { - return { - type: FAVOURITED_STATUSES_FETCH_REQUEST, - }; -}; - -export function fetchFavouritedStatusesSuccess(statuses, next) { - return { - type: FAVOURITED_STATUSES_FETCH_SUCCESS, - statuses, - next, - }; -}; - -export function fetchFavouritedStatusesFail(error) { - return { - type: FAVOURITED_STATUSES_FETCH_FAIL, - error, - }; -}; - -export function expandFavouritedStatuses() { - return (dispatch, getState) => { - const url = getState().getIn(['status_lists', 'favourites', 'next'], null); - - if (url === null) { - return; - } - - dispatch(expandFavouritedStatusesRequest()); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(expandFavouritedStatusesFail(error)); - }); - }; -}; - -export function expandFavouritedStatusesRequest() { - return { - type: FAVOURITED_STATUSES_EXPAND_REQUEST, - }; -}; - -export function expandFavouritedStatusesSuccess(statuses, next) { - return { - type: FAVOURITED_STATUSES_EXPAND_SUCCESS, - statuses, - next, - }; -}; - -export function expandFavouritedStatusesFail(error) { - return { - type: FAVOURITED_STATUSES_EXPAND_FAIL, - error, - }; -}; diff --git a/app/javascript/themes/glitch/actions/height_cache.js b/app/javascript/themes/glitch/actions/height_cache.js deleted file mode 100644 index 4c752993f..000000000 --- a/app/javascript/themes/glitch/actions/height_cache.js +++ /dev/null @@ -1,17 +0,0 @@ -export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET'; -export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR'; - -export function setHeight (key, id, height) { - return { - type: HEIGHT_CACHE_SET, - key, - id, - height, - }; -}; - -export function clearHeight () { - return { - type: HEIGHT_CACHE_CLEAR, - }; -}; diff --git a/app/javascript/themes/glitch/actions/interactions.js b/app/javascript/themes/glitch/actions/interactions.js deleted file mode 100644 index d61a7ba2a..000000000 --- a/app/javascript/themes/glitch/actions/interactions.js +++ /dev/null @@ -1,313 +0,0 @@ -import api from 'themes/glitch/util/api'; - -export const REBLOG_REQUEST = 'REBLOG_REQUEST'; -export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; -export const REBLOG_FAIL = 'REBLOG_FAIL'; - -export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST'; -export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS'; -export const FAVOURITE_FAIL = 'FAVOURITE_FAIL'; - -export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST'; -export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS'; -export const UNREBLOG_FAIL = 'UNREBLOG_FAIL'; - -export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST'; -export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS'; -export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL'; - -export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST'; -export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS'; -export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL'; - -export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST'; -export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; -export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; - -export const PIN_REQUEST = 'PIN_REQUEST'; -export const PIN_SUCCESS = 'PIN_SUCCESS'; -export const PIN_FAIL = 'PIN_FAIL'; - -export const UNPIN_REQUEST = 'UNPIN_REQUEST'; -export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; -export const UNPIN_FAIL = 'UNPIN_FAIL'; - -export function reblog(status) { - return function (dispatch, getState) { - dispatch(reblogRequest(status)); - - api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) { - // The reblog API method returns a new status wrapped around the original. In this case we are only - // interested in how the original is modified, hence passing it skipping the wrapper - dispatch(reblogSuccess(status, response.data.reblog)); - }).catch(function (error) { - dispatch(reblogFail(status, error)); - }); - }; -}; - -export function unreblog(status) { - return (dispatch, getState) => { - dispatch(unreblogRequest(status)); - - api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => { - dispatch(unreblogSuccess(status, response.data)); - }).catch(error => { - dispatch(unreblogFail(status, error)); - }); - }; -}; - -export function reblogRequest(status) { - return { - type: REBLOG_REQUEST, - status: status, - }; -}; - -export function reblogSuccess(status, response) { - return { - type: REBLOG_SUCCESS, - status: status, - response: response, - }; -}; - -export function reblogFail(status, error) { - return { - type: REBLOG_FAIL, - status: status, - error: error, - }; -}; - -export function unreblogRequest(status) { - return { - type: UNREBLOG_REQUEST, - status: status, - }; -}; - -export function unreblogSuccess(status, response) { - return { - type: UNREBLOG_SUCCESS, - status: status, - response: response, - }; -}; - -export function unreblogFail(status, error) { - return { - type: UNREBLOG_FAIL, - status: status, - error: error, - }; -}; - -export function favourite(status) { - return function (dispatch, getState) { - dispatch(favouriteRequest(status)); - - api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) { - dispatch(favouriteSuccess(status, response.data)); - }).catch(function (error) { - dispatch(favouriteFail(status, error)); - }); - }; -}; - -export function unfavourite(status) { - return (dispatch, getState) => { - dispatch(unfavouriteRequest(status)); - - api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => { - dispatch(unfavouriteSuccess(status, response.data)); - }).catch(error => { - dispatch(unfavouriteFail(status, error)); - }); - }; -}; - -export function favouriteRequest(status) { - return { - type: FAVOURITE_REQUEST, - status: status, - }; -}; - -export function favouriteSuccess(status, response) { - return { - type: FAVOURITE_SUCCESS, - status: status, - response: response, - }; -}; - -export function favouriteFail(status, error) { - return { - type: FAVOURITE_FAIL, - status: status, - error: error, - }; -}; - -export function unfavouriteRequest(status) { - return { - type: UNFAVOURITE_REQUEST, - status: status, - }; -}; - -export function unfavouriteSuccess(status, response) { - return { - type: UNFAVOURITE_SUCCESS, - status: status, - response: response, - }; -}; - -export function unfavouriteFail(status, error) { - return { - type: UNFAVOURITE_FAIL, - status: status, - error: error, - }; -}; - -export function fetchReblogs(id) { - return (dispatch, getState) => { - dispatch(fetchReblogsRequest(id)); - - api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => { - dispatch(fetchReblogsSuccess(id, response.data)); - }).catch(error => { - dispatch(fetchReblogsFail(id, error)); - }); - }; -}; - -export function fetchReblogsRequest(id) { - return { - type: REBLOGS_FETCH_REQUEST, - id, - }; -}; - -export function fetchReblogsSuccess(id, accounts) { - return { - type: REBLOGS_FETCH_SUCCESS, - id, - accounts, - }; -}; - -export function fetchReblogsFail(id, error) { - return { - type: REBLOGS_FETCH_FAIL, - error, - }; -}; - -export function fetchFavourites(id) { - return (dispatch, getState) => { - dispatch(fetchFavouritesRequest(id)); - - api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => { - dispatch(fetchFavouritesSuccess(id, response.data)); - }).catch(error => { - dispatch(fetchFavouritesFail(id, error)); - }); - }; -}; - -export function fetchFavouritesRequest(id) { - return { - type: FAVOURITES_FETCH_REQUEST, - id, - }; -}; - -export function fetchFavouritesSuccess(id, accounts) { - return { - type: FAVOURITES_FETCH_SUCCESS, - id, - accounts, - }; -}; - -export function fetchFavouritesFail(id, error) { - return { - type: FAVOURITES_FETCH_FAIL, - error, - }; -}; - -export function pin(status) { - return (dispatch, getState) => { - dispatch(pinRequest(status)); - - api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { - dispatch(pinSuccess(status, response.data)); - }).catch(error => { - dispatch(pinFail(status, error)); - }); - }; -}; - -export function pinRequest(status) { - return { - type: PIN_REQUEST, - status, - }; -}; - -export function pinSuccess(status, response) { - return { - type: PIN_SUCCESS, - status, - response, - }; -}; - -export function pinFail(status, error) { - return { - type: PIN_FAIL, - status, - error, - }; -}; - -export function unpin (status) { - return (dispatch, getState) => { - dispatch(unpinRequest(status)); - - api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { - dispatch(unpinSuccess(status, response.data)); - }).catch(error => { - dispatch(unpinFail(status, error)); - }); - }; -}; - -export function unpinRequest(status) { - return { - type: UNPIN_REQUEST, - status, - }; -}; - -export function unpinSuccess(status, response) { - return { - type: UNPIN_SUCCESS, - status, - response, - }; -}; - -export function unpinFail(status, error) { - return { - type: UNPIN_FAIL, - status, - error, - }; -}; diff --git a/app/javascript/themes/glitch/actions/local_settings.js b/app/javascript/themes/glitch/actions/local_settings.js deleted file mode 100644 index 28660a4e8..000000000 --- a/app/javascript/themes/glitch/actions/local_settings.js +++ /dev/null @@ -1,24 +0,0 @@ -export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; - -export function changeLocalSetting(key, value) { - return dispatch => { - dispatch({ - type: LOCAL_SETTING_CHANGE, - key, - value, - }); - - dispatch(saveLocalSettings()); - }; -}; - -// __TODO :__ -// Right now `saveLocalSettings()` doesn't keep track of which user -// is currently signed in, but it might be better to give each user -// their *own* local settings. -export function saveLocalSettings() { - return (_, getState) => { - const localSettings = getState().get('local_settings').toJS(); - localStorage.setItem('mastodon-settings', JSON.stringify(localSettings)); - }; -}; diff --git a/app/javascript/themes/glitch/actions/modal.js b/app/javascript/themes/glitch/actions/modal.js deleted file mode 100644 index 80e15c28e..000000000 --- a/app/javascript/themes/glitch/actions/modal.js +++ /dev/null @@ -1,16 +0,0 @@ -export const MODAL_OPEN = 'MODAL_OPEN'; -export const MODAL_CLOSE = 'MODAL_CLOSE'; - -export function openModal(type, props) { - return { - type: MODAL_OPEN, - modalType: type, - modalProps: props, - }; -}; - -export function closeModal() { - return { - type: MODAL_CLOSE, - }; -}; diff --git a/app/javascript/themes/glitch/actions/mutes.js b/app/javascript/themes/glitch/actions/mutes.js deleted file mode 100644 index bb19e8657..000000000 --- a/app/javascript/themes/glitch/actions/mutes.js +++ /dev/null @@ -1,103 +0,0 @@ -import api, { getLinks } from 'themes/glitch/util/api'; -import { fetchRelationships } from './accounts'; -import { openModal } from 'themes/glitch/actions/modal'; - -export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; -export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; -export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL'; - -export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST'; -export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; -export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; - -export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; -export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; - -export function fetchMutes() { - return (dispatch, getState) => { - dispatch(fetchMutesRequest()); - - api(getState).get('/api/v1/mutes').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchMutesSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(fetchMutesFail(error))); - }; -}; - -export function fetchMutesRequest() { - return { - type: MUTES_FETCH_REQUEST, - }; -}; - -export function fetchMutesSuccess(accounts, next) { - return { - type: MUTES_FETCH_SUCCESS, - accounts, - next, - }; -}; - -export function fetchMutesFail(error) { - return { - type: MUTES_FETCH_FAIL, - error, - }; -}; - -export function expandMutes() { - return (dispatch, getState) => { - const url = getState().getIn(['user_lists', 'mutes', 'next']); - - if (url === null) { - return; - } - - dispatch(expandMutesRequest()); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandMutesSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(expandMutesFail(error))); - }; -}; - -export function expandMutesRequest() { - return { - type: MUTES_EXPAND_REQUEST, - }; -}; - -export function expandMutesSuccess(accounts, next) { - return { - type: MUTES_EXPAND_SUCCESS, - accounts, - next, - }; -}; - -export function expandMutesFail(error) { - return { - type: MUTES_EXPAND_FAIL, - error, - }; -}; - -export function initMuteModal(account) { - return dispatch => { - dispatch({ - type: MUTES_INIT_MODAL, - account, - }); - - dispatch(openModal('MUTE')); - }; -} - -export function toggleHideNotifications() { - return dispatch => { - dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); - }; -} diff --git a/app/javascript/themes/glitch/actions/notifications.js b/app/javascript/themes/glitch/actions/notifications.js deleted file mode 100644 index fbf06f7c4..000000000 --- a/app/javascript/themes/glitch/actions/notifications.js +++ /dev/null @@ -1,265 +0,0 @@ -import api, { getLinks } from 'themes/glitch/util/api'; -import { List as ImmutableList } from 'immutable'; -import IntlMessageFormat from 'intl-messageformat'; -import { fetchRelationships } from './accounts'; -import { defineMessages } from 'react-intl'; - -export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; - -// tracking the notif cleaning request -export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST'; -export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS'; -export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL'; -export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = 'NOTIFICATIONS_MARK_ALL_FOR_DELETE'; -export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes -// Unmark notifications (when the cleaning mode is left) -export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE'; -// Mark one for delete -export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE'; - -export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST'; -export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS'; -export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL'; - -export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; -export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; -export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; - -export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; - -defineMessages({ - mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, -}); - -const fetchRelatedRelationships = (dispatch, notifications) => { - const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id); - - if (accountIds > 0) { - dispatch(fetchRelationships(accountIds)); - } -}; - -const unescapeHTML = (html) => { - const wrapper = document.createElement('div'); - html = html.replace(/<br \/>|<br>|\n/, ' '); - wrapper.innerHTML = html; - return wrapper.textContent; -}; - -export function updateNotifications(notification, intlMessages, intlLocale) { - return (dispatch, getState) => { - const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); - const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); - - dispatch({ - type: NOTIFICATIONS_UPDATE, - notification, - account: notification.account, - status: notification.status, - meta: playSound ? { sound: 'boop' } : undefined, - }); - - fetchRelatedRelationships(dispatch, [notification]); - - // Desktop notifications - if (typeof window.Notification !== 'undefined' && showAlert) { - const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); - const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : ''); - - const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id }); - notify.addEventListener('click', () => { - window.focus(); - notify.close(); - }); - } - }; -}; - -const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); - -export function refreshNotifications() { - return (dispatch, getState) => { - const params = {}; - const ids = getState().getIn(['notifications', 'items']); - - let skipLoading = false; - - if (ids.size > 0) { - params.since_id = ids.first().get('id'); - } - - if (getState().getIn(['notifications', 'loaded'])) { - skipLoading = true; - } - - params.exclude_types = excludeTypesFromSettings(getState()); - - dispatch(refreshNotificationsRequest(skipLoading)); - - api(getState).get('/api/v1/notifications', { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(refreshNotificationsSuccess(response.data, skipLoading, next ? next.uri : null)); - fetchRelatedRelationships(dispatch, response.data); - }).catch(error => { - dispatch(refreshNotificationsFail(error, skipLoading)); - }); - }; -}; - -export function refreshNotificationsRequest(skipLoading) { - return { - type: NOTIFICATIONS_REFRESH_REQUEST, - skipLoading, - }; -}; - -export function refreshNotificationsSuccess(notifications, skipLoading, next) { - return { - type: NOTIFICATIONS_REFRESH_SUCCESS, - notifications, - accounts: notifications.map(item => item.account), - statuses: notifications.map(item => item.status).filter(status => !!status), - skipLoading, - next, - }; -}; - -export function refreshNotificationsFail(error, skipLoading) { - return { - type: NOTIFICATIONS_REFRESH_FAIL, - error, - skipLoading, - }; -}; - -export function expandNotifications() { - return (dispatch, getState) => { - const items = getState().getIn(['notifications', 'items'], ImmutableList()); - - if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) { - return; - } - - const params = { - max_id: items.last().get('id'), - limit: 20, - exclude_types: excludeTypesFromSettings(getState()), - }; - - dispatch(expandNotificationsRequest()); - - api(getState).get('/api/v1/notifications', { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); - fetchRelatedRelationships(dispatch, response.data); - }).catch(error => { - dispatch(expandNotificationsFail(error)); - }); - }; -}; - -export function expandNotificationsRequest() { - return { - type: NOTIFICATIONS_EXPAND_REQUEST, - }; -}; - -export function expandNotificationsSuccess(notifications, next) { - return { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications, - accounts: notifications.map(item => item.account), - statuses: notifications.map(item => item.status).filter(status => !!status), - next, - }; -}; - -export function expandNotificationsFail(error) { - return { - type: NOTIFICATIONS_EXPAND_FAIL, - error, - }; -}; - -export function clearNotifications() { - return (dispatch, getState) => { - dispatch({ - type: NOTIFICATIONS_CLEAR, - }); - - api(getState).post('/api/v1/notifications/clear'); - }; -}; - -export function scrollTopNotifications(top) { - return { - type: NOTIFICATIONS_SCROLL_TOP, - top, - }; -}; - -export function deleteMarkedNotifications() { - return (dispatch, getState) => { - dispatch(deleteMarkedNotificationsRequest()); - - let ids = []; - getState().getIn(['notifications', 'items']).forEach((n) => { - if (n.get('markedForDelete')) { - ids.push(n.get('id')); - } - }); - - if (ids.length === 0) { - return; - } - - api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => { - dispatch(deleteMarkedNotificationsSuccess()); - }).catch(error => { - console.error(error); - dispatch(deleteMarkedNotificationsFail(error)); - }); - }; -}; - -export function enterNotificationClearingMode(yes) { - return { - type: NOTIFICATIONS_ENTER_CLEARING_MODE, - yes: yes, - }; -}; - -export function markAllNotifications(yes) { - return { - type: NOTIFICATIONS_MARK_ALL_FOR_DELETE, - yes: yes, // true, false or null. null = invert - }; -}; - -export function deleteMarkedNotificationsRequest() { - return { - type: NOTIFICATIONS_DELETE_MARKED_REQUEST, - }; -}; - -export function deleteMarkedNotificationsFail() { - return { - type: NOTIFICATIONS_DELETE_MARKED_FAIL, - }; -}; - -export function markNotificationForDelete(id, yes) { - return { - type: NOTIFICATION_MARK_FOR_DELETE, - id: id, - yes: yes, - }; -}; - -export function deleteMarkedNotificationsSuccess() { - return { - type: NOTIFICATIONS_DELETE_MARKED_SUCCESS, - }; -}; diff --git a/app/javascript/themes/glitch/actions/onboarding.js b/app/javascript/themes/glitch/actions/onboarding.js deleted file mode 100644 index a161c50ef..000000000 --- a/app/javascript/themes/glitch/actions/onboarding.js +++ /dev/null @@ -1,14 +0,0 @@ -import { openModal } from './modal'; -import { changeSetting, saveSettings } from './settings'; - -export function showOnboardingOnce() { - return (dispatch, getState) => { - const alreadySeen = getState().getIn(['settings', 'onboarded']); - - if (!alreadySeen) { - dispatch(openModal('ONBOARDING')); - dispatch(changeSetting(['onboarded'], true)); - dispatch(saveSettings()); - } - }; -}; diff --git a/app/javascript/themes/glitch/actions/pin_statuses.js b/app/javascript/themes/glitch/actions/pin_statuses.js deleted file mode 100644 index b3e064e58..000000000 --- a/app/javascript/themes/glitch/actions/pin_statuses.js +++ /dev/null @@ -1,40 +0,0 @@ -import api from 'themes/glitch/util/api'; - -export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; -export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; -export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; - -import { me } from 'themes/glitch/util/initial_state'; - -export function fetchPinnedStatuses() { - return (dispatch, getState) => { - dispatch(fetchPinnedStatusesRequest()); - - api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { - dispatch(fetchPinnedStatusesSuccess(response.data, null)); - }).catch(error => { - dispatch(fetchPinnedStatusesFail(error)); - }); - }; -}; - -export function fetchPinnedStatusesRequest() { - return { - type: PINNED_STATUSES_FETCH_REQUEST, - }; -}; - -export function fetchPinnedStatusesSuccess(statuses, next) { - return { - type: PINNED_STATUSES_FETCH_SUCCESS, - statuses, - next, - }; -}; - -export function fetchPinnedStatusesFail(error) { - return { - type: PINNED_STATUSES_FETCH_FAIL, - error, - }; -}; diff --git a/app/javascript/themes/glitch/actions/push_notifications.js b/app/javascript/themes/glitch/actions/push_notifications.js deleted file mode 100644 index 55661d2b0..000000000 --- a/app/javascript/themes/glitch/actions/push_notifications.js +++ /dev/null @@ -1,52 +0,0 @@ -import axios from 'axios'; - -export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; -export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; -export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION'; -export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE'; - -export function setBrowserSupport (value) { - return { - type: SET_BROWSER_SUPPORT, - value, - }; -} - -export function setSubscription (subscription) { - return { - type: SET_SUBSCRIPTION, - subscription, - }; -} - -export function clearSubscription () { - return { - type: CLEAR_SUBSCRIPTION, - }; -} - -export function changeAlerts(key, value) { - return dispatch => { - dispatch({ - type: ALERTS_CHANGE, - key, - value, - }); - - dispatch(saveSettings()); - }; -} - -export function saveSettings() { - return (_, getState) => { - const state = getState().get('push_notifications'); - const subscription = state.get('subscription'); - const alerts = state.get('alerts'); - - axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { - data: { - alerts, - }, - }); - }; -} diff --git a/app/javascript/themes/glitch/actions/reports.js b/app/javascript/themes/glitch/actions/reports.js deleted file mode 100644 index 93f9085b2..000000000 --- a/app/javascript/themes/glitch/actions/reports.js +++ /dev/null @@ -1,80 +0,0 @@ -import api from 'themes/glitch/util/api'; -import { openModal, closeModal } from './modal'; - -export const REPORT_INIT = 'REPORT_INIT'; -export const REPORT_CANCEL = 'REPORT_CANCEL'; - -export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; -export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; -export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; - -export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE'; -export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE'; - -export function initReport(account, status) { - return dispatch => { - dispatch({ - type: REPORT_INIT, - account, - status, - }); - - dispatch(openModal('REPORT')); - }; -}; - -export function cancelReport() { - return { - type: REPORT_CANCEL, - }; -}; - -export function toggleStatusReport(statusId, checked) { - return { - type: REPORT_STATUS_TOGGLE, - statusId, - checked, - }; -}; - -export function submitReport() { - return (dispatch, getState) => { - dispatch(submitReportRequest()); - - api(getState).post('/api/v1/reports', { - account_id: getState().getIn(['reports', 'new', 'account_id']), - status_ids: getState().getIn(['reports', 'new', 'status_ids']), - comment: getState().getIn(['reports', 'new', 'comment']), - }).then(response => { - dispatch(closeModal()); - dispatch(submitReportSuccess(response.data)); - }).catch(error => dispatch(submitReportFail(error))); - }; -}; - -export function submitReportRequest() { - return { - type: REPORT_SUBMIT_REQUEST, - }; -}; - -export function submitReportSuccess(report) { - return { - type: REPORT_SUBMIT_SUCCESS, - report, - }; -}; - -export function submitReportFail(error) { - return { - type: REPORT_SUBMIT_FAIL, - error, - }; -}; - -export function changeReportComment(comment) { - return { - type: REPORT_COMMENT_CHANGE, - comment, - }; -}; diff --git a/app/javascript/themes/glitch/actions/search.js b/app/javascript/themes/glitch/actions/search.js deleted file mode 100644 index 414e4755e..000000000 --- a/app/javascript/themes/glitch/actions/search.js +++ /dev/null @@ -1,73 +0,0 @@ -import api from 'themes/glitch/util/api'; - -export const SEARCH_CHANGE = 'SEARCH_CHANGE'; -export const SEARCH_CLEAR = 'SEARCH_CLEAR'; -export const SEARCH_SHOW = 'SEARCH_SHOW'; - -export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; -export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; -export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; - -export function changeSearch(value) { - return { - type: SEARCH_CHANGE, - value, - }; -}; - -export function clearSearch() { - return { - type: SEARCH_CLEAR, - }; -}; - -export function submitSearch() { - return (dispatch, getState) => { - const value = getState().getIn(['search', 'value']); - - if (value.length === 0) { - return; - } - - dispatch(fetchSearchRequest()); - - api(getState).get('/api/v1/search', { - params: { - q: value, - resolve: true, - }, - }).then(response => { - dispatch(fetchSearchSuccess(response.data)); - }).catch(error => { - dispatch(fetchSearchFail(error)); - }); - }; -}; - -export function fetchSearchRequest() { - return { - type: SEARCH_FETCH_REQUEST, - }; -}; - -export function fetchSearchSuccess(results) { - return { - type: SEARCH_FETCH_SUCCESS, - results, - accounts: results.accounts, - statuses: results.statuses, - }; -}; - -export function fetchSearchFail(error) { - return { - type: SEARCH_FETCH_FAIL, - error, - }; -}; - -export function showSearch() { - return { - type: SEARCH_SHOW, - }; -}; diff --git a/app/javascript/themes/glitch/actions/settings.js b/app/javascript/themes/glitch/actions/settings.js deleted file mode 100644 index 79adca18c..000000000 --- a/app/javascript/themes/glitch/actions/settings.js +++ /dev/null @@ -1,31 +0,0 @@ -import axios from 'axios'; -import { debounce } from 'lodash'; - -export const SETTING_CHANGE = 'SETTING_CHANGE'; -export const SETTING_SAVE = 'SETTING_SAVE'; - -export function changeSetting(key, value) { - return dispatch => { - dispatch({ - type: SETTING_CHANGE, - key, - value, - }); - - dispatch(saveSettings()); - }; -}; - -const debouncedSave = debounce((dispatch, getState) => { - if (getState().getIn(['settings', 'saved'])) { - return; - } - - const data = getState().get('settings').filter((_, key) => key !== 'saved').toJS(); - - axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE })); -}, 5000, { trailing: true }); - -export function saveSettings() { - return (dispatch, getState) => debouncedSave(dispatch, getState); -}; diff --git a/app/javascript/themes/glitch/actions/statuses.js b/app/javascript/themes/glitch/actions/statuses.js deleted file mode 100644 index 702f4e9b6..000000000 --- a/app/javascript/themes/glitch/actions/statuses.js +++ /dev/null @@ -1,217 +0,0 @@ -import api from 'themes/glitch/util/api'; - -import { deleteFromTimelines } from './timelines'; -import { fetchStatusCard } from './cards'; - -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_DELETE_REQUEST = 'STATUS_DELETE_REQUEST'; -export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; -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 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_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; -export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; -export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; - -export function fetchStatusRequest(id, skipLoading) { - return { - type: STATUS_FETCH_REQUEST, - id, - skipLoading, - }; -}; - -export function fetchStatus(id) { - return (dispatch, getState) => { - const skipLoading = getState().getIn(['statuses', id], null) !== null; - - dispatch(fetchContext(id)); - dispatch(fetchStatusCard(id)); - - if (skipLoading) { - return; - } - - dispatch(fetchStatusRequest(id, skipLoading)); - - api(getState).get(`/api/v1/statuses/${id}`).then(response => { - dispatch(fetchStatusSuccess(response.data, skipLoading)); - }).catch(error => { - dispatch(fetchStatusFail(id, error, skipLoading)); - }); - }; -}; - -export function fetchStatusSuccess(status, skipLoading) { - return { - type: STATUS_FETCH_SUCCESS, - status, - skipLoading, - }; -}; - -export function fetchStatusFail(id, error, skipLoading) { - return { - type: STATUS_FETCH_FAIL, - id, - error, - skipLoading, - skipAlert: true, - }; -}; - -export function deleteStatus(id) { - return (dispatch, getState) => { - dispatch(deleteStatusRequest(id)); - - api(getState).delete(`/api/v1/statuses/${id}`).then(() => { - dispatch(deleteStatusSuccess(id)); - dispatch(deleteFromTimelines(id)); - }).catch(error => { - dispatch(deleteStatusFail(id, error)); - }); - }; -}; - -export function deleteStatusRequest(id) { - return { - type: STATUS_DELETE_REQUEST, - id: id, - }; -}; - -export function deleteStatusSuccess(id) { - return { - type: STATUS_DELETE_SUCCESS, - id: id, - }; -}; - -export function deleteStatusFail(id, error) { - return { - type: STATUS_DELETE_FAIL, - id: id, - error: error, - }; -}; - -export function fetchContext(id) { - return (dispatch, getState) => { - dispatch(fetchContextRequest(id)); - - api(getState).get(`/api/v1/statuses/${id}/context`).then(response => { - dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants)); - - }).catch(error => { - if (error.response && error.response.status === 404) { - dispatch(deleteFromTimelines(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, - id, - ancestors, - descendants, - statuses: ancestors.concat(descendants), - }; -}; - -export function fetchContextFail(id, error) { - return { - type: CONTEXT_FETCH_FAIL, - id, - error, - skipAlert: true, - }; -}; - -export function muteStatus(id) { - return (dispatch, getState) => { - dispatch(muteStatusRequest(id)); - - api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => { - dispatch(muteStatusSuccess(id)); - }).catch(error => { - 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 { - type: STATUS_MUTE_FAIL, - id, - error, - }; -}; - -export function unmuteStatus(id) { - return (dispatch, getState) => { - dispatch(unmuteStatusRequest(id)); - - api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => { - dispatch(unmuteStatusSuccess(id)); - }).catch(error => { - 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 { - type: STATUS_UNMUTE_FAIL, - id, - error, - }; -}; diff --git a/app/javascript/themes/glitch/actions/store.js b/app/javascript/themes/glitch/actions/store.js deleted file mode 100644 index a1db0fdd5..000000000 --- a/app/javascript/themes/glitch/actions/store.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Iterable, fromJS } from 'immutable'; - -export const STORE_HYDRATE = 'STORE_HYDRATE'; -export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; - -const convertState = rawState => - fromJS(rawState, (k, v) => - Iterable.isIndexed(v) ? v.toList() : v.toMap()); - -export function hydrateStore(rawState) { - const state = convertState(rawState); - - return { - type: STORE_HYDRATE, - state, - }; -}; diff --git a/app/javascript/themes/glitch/actions/streaming.js b/app/javascript/themes/glitch/actions/streaming.js deleted file mode 100644 index ccf6c27d8..000000000 --- a/app/javascript/themes/glitch/actions/streaming.js +++ /dev/null @@ -1,54 +0,0 @@ -import { connectStream } from 'themes/glitch/util/stream'; -import { - updateTimeline, - deleteFromTimelines, - refreshHomeTimeline, - connectTimeline, - disconnectTimeline, -} from './timelines'; -import { updateNotifications, refreshNotifications } from './notifications'; -import { getLocale } from 'mastodon/locales'; - -const { messages } = getLocale(); - -export function connectTimelineStream (timelineId, path, pollingRefresh = null) { - - return connectStream (path, pollingRefresh, (dispatch, getState) => { - const locale = getState().getIn(['meta', 'locale']); - return { - onConnect() { - dispatch(connectTimeline(timelineId)); - }, - - onDisconnect() { - dispatch(disconnectTimeline(timelineId)); - }, - - onReceive (data) { - switch(data.event) { - case 'update': - dispatch(updateTimeline(timelineId, JSON.parse(data.payload))); - break; - case 'delete': - dispatch(deleteFromTimelines(data.payload)); - break; - case 'notification': - dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); - break; - } - }, - }; - }); -} - -function refreshHomeTimelineAndNotification (dispatch) { - dispatch(refreshHomeTimeline()); - dispatch(refreshNotifications()); -} - -export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); -export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); -export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); -export const connectPublicStream = () => connectTimelineStream('public', 'public'); -export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); -export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); diff --git a/app/javascript/themes/glitch/actions/timelines.js b/app/javascript/themes/glitch/actions/timelines.js deleted file mode 100644 index 5ce14fbe9..000000000 --- a/app/javascript/themes/glitch/actions/timelines.js +++ /dev/null @@ -1,208 +0,0 @@ -import api, { getLinks } from 'themes/glitch/util/api'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; -export const TIMELINE_DELETE = 'TIMELINE_DELETE'; - -export const TIMELINE_REFRESH_REQUEST = 'TIMELINE_REFRESH_REQUEST'; -export const TIMELINE_REFRESH_SUCCESS = 'TIMELINE_REFRESH_SUCCESS'; -export const TIMELINE_REFRESH_FAIL = 'TIMELINE_REFRESH_FAIL'; - -export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; -export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; -export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; - -export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; - -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; - -export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE'; - -export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) { - return { - type: TIMELINE_REFRESH_SUCCESS, - timeline, - statuses, - skipLoading, - next, - }; -}; - -export function updateTimeline(timeline, status) { - return (dispatch, getState) => { - const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : []; - const parents = []; - - if (status.in_reply_to_id) { - let parent = getState().getIn(['statuses', status.in_reply_to_id]); - - while (parent && parent.get('in_reply_to_id')) { - parents.push(parent.get('id')); - parent = getState().getIn(['statuses', parent.get('in_reply_to_id')]); - } - } - - dispatch({ - type: TIMELINE_UPDATE, - timeline, - status, - references, - }); - - if (parents.length > 0) { - dispatch({ - type: TIMELINE_CONTEXT_UPDATE, - status, - references: parents, - }); - } - }; -}; - -export function deleteFromTimelines(id) { - return (dispatch, getState) => { - const accountId = getState().getIn(['statuses', id, 'account']); - const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]); - const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); - - dispatch({ - type: TIMELINE_DELETE, - id, - accountId, - references, - reblogOf, - }); - }; -}; - -export function refreshTimelineRequest(timeline, skipLoading) { - return { - type: TIMELINE_REFRESH_REQUEST, - timeline, - skipLoading, - }; -}; - -export function refreshTimeline(timelineId, path, params = {}) { - return function (dispatch, getState) { - const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); - - if (timeline.get('isLoading') || timeline.get('online')) { - return; - } - - const ids = timeline.get('items', ImmutableList()); - const newestId = ids.size > 0 ? ids.first() : null; - - let skipLoading = timeline.get('loaded'); - - if (newestId !== null) { - params.since_id = newestId; - } - - dispatch(refreshTimelineRequest(timelineId, skipLoading)); - - api(getState).get(path, { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null)); - }).catch(error => { - dispatch(refreshTimelineFail(timelineId, error, skipLoading)); - }); - }; -}; - -export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home'); -export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public'); -export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true }); -export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct'); -export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); -export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); -export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); - -export function refreshTimelineFail(timeline, error, skipLoading) { - return { - type: TIMELINE_REFRESH_FAIL, - timeline, - error, - skipLoading, - skipAlert: error.response && error.response.status === 404, - }; -}; - -export function expandTimeline(timelineId, path, params = {}) { - return (dispatch, getState) => { - const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); - const ids = timeline.get('items', ImmutableList()); - - if (timeline.get('isLoading') || ids.size === 0) { - return; - } - - params.max_id = ids.last(); - params.limit = 10; - - dispatch(expandTimelineRequest(timelineId)); - - api(getState).get(path, { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(expandTimelineFail(timelineId, error)); - }); - }; -}; - -export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home'); -export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public'); -export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true }); -export const expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct'); -export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); -export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); -export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); - -export function expandTimelineRequest(timeline) { - return { - type: TIMELINE_EXPAND_REQUEST, - timeline, - }; -}; - -export function expandTimelineSuccess(timeline, statuses, next) { - return { - type: TIMELINE_EXPAND_SUCCESS, - timeline, - statuses, - next, - }; -}; - -export function expandTimelineFail(timeline, error) { - return { - type: TIMELINE_EXPAND_FAIL, - timeline, - error, - }; -}; - -export function scrollTopTimeline(timeline, top) { - return { - type: TIMELINE_SCROLL_TOP, - timeline, - top, - }; -}; - -export function connectTimeline(timeline) { - return { - type: TIMELINE_CONNECT, - timeline, - }; -}; - -export function disconnectTimeline(timeline) { - return { - type: TIMELINE_DISCONNECT, - timeline, - }; -}; diff --git a/app/javascript/themes/glitch/components/account.js b/app/javascript/themes/glitch/components/account.js deleted file mode 100644 index d0ff77050..000000000 --- a/app/javascript/themes/glitch/components/account.js +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from './avatar'; -import DisplayName from './display_name'; -import Permalink from './permalink'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'You are not currently muting notifications from @{name}. Click to mute notifications' }, - unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' }, -}); - -@injectIntl -export default class Account extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hidden: PropTypes.bool, - }; - - 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); - } - - render () { - const { account, intl, hidden } = this.props; - - if (!account) { - return <div />; - } - - if (hidden) { - return ( - <div> - {account.get('display_name')} - {account.get('username')} - </div> - ); - } - - let buttons; - - if (account.get('id') !== me && account.get('relationship', null) !== null) { - const following = account.getIn(['relationship', 'following']); - const requested = account.getIn(['relationship', 'requested']); - const blocking = account.getIn(['relationship', 'blocking']); - const muting = account.getIn(['relationship', 'muting']); - - if (requested) { - buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; - } else if (blocking) { - buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; - } else if (muting) { - let hidingNotificationsButton; - if (muting.get('notifications')) { - hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />; - } else { - hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />; - } - buttons = ( - <div> - <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} /> - {hidingNotificationsButton} - </div> - ); - } else { - buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />; - } - } - - return ( - <div className='account'> - <div className='account__wrapper'> - <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> - <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div> - <DisplayName account={account} /> - </Permalink> - - <div className='account__relationship'> - {buttons} - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/attachment_list.js b/app/javascript/themes/glitch/components/attachment_list.js deleted file mode 100644 index b3d00b335..000000000 --- a/app/javascript/themes/glitch/components/attachment_list.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; - -export default class AttachmentList extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.list.isRequired, - }; - - render () { - const { media } = this.props; - - return ( - <div className='attachment-list'> - <div className='attachment-list__icon'> - <i className='fa fa-link' /> - </div> - - <ul className='attachment-list__list'> - {media.map(attachment => - <li key={attachment.get('id')}> - <a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a> - </li> - )} - </ul> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/autosuggest_emoji.js b/app/javascript/themes/glitch/components/autosuggest_emoji.js deleted file mode 100644 index 3c6f915e4..000000000 --- a/app/javascript/themes/glitch/components/autosuggest_emoji.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import unicodeMapping from 'themes/glitch/util/emoji/emoji_unicode_mapping_light'; - -const assetHost = process.env.CDN_HOST || ''; - -export default class AutosuggestEmoji extends React.PureComponent { - - static propTypes = { - emoji: PropTypes.object.isRequired, - }; - - render () { - const { emoji } = this.props; - let url; - - if (emoji.custom) { - url = emoji.imageUrl; - } else { - const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')]; - - if (!mapping) { - return null; - } - - url = `${assetHost}/emoji/${mapping.filename}.svg`; - } - - return ( - <div className='autosuggest-emoji'> - <img - className='emojione' - src={url} - alt={emoji.native || emoji.colons} - /> - - {emoji.colons} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/autosuggest_textarea.js b/app/javascript/themes/glitch/components/autosuggest_textarea.js deleted file mode 100644 index fa93847a2..000000000 --- a/app/javascript/themes/glitch/components/autosuggest_textarea.js +++ /dev/null @@ -1,222 +0,0 @@ -import React from 'react'; -import AutosuggestAccountContainer from 'themes/glitch/features/compose/containers/autosuggest_account_container'; -import AutosuggestEmoji from './autosuggest_emoji'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { isRtl } from 'themes/glitch/util/rtl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Textarea from 'react-textarea-autosize'; -import classNames from 'classnames'; - -const textAtCursorMatchesToken = (str, caretPosition) => { - let word; - - let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/); - let right = str.slice(caretPosition).search(/[\s\u200B]/); - - if (right < 0) { - word = str.slice(left); - } else { - word = str.slice(left, right + caretPosition); - } - - if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) { - return [null, null]; - } - - word = word.trim().toLowerCase(); - - if (word.length > 0) { - return [left + 1, word]; - } else { - return [null, null]; - } -}; - -export default class AutosuggestTextarea extends ImmutablePureComponent { - - static propTypes = { - value: PropTypes.string, - suggestions: ImmutablePropTypes.list, - disabled: PropTypes.bool, - placeholder: PropTypes.string, - onSuggestionSelected: PropTypes.func.isRequired, - onSuggestionsClearRequested: PropTypes.func.isRequired, - onSuggestionsFetchRequested: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onKeyUp: PropTypes.func, - onKeyDown: PropTypes.func, - onPaste: PropTypes.func.isRequired, - autoFocus: PropTypes.bool, - }; - - static defaultProps = { - autoFocus: true, - }; - - state = { - suggestionsHidden: false, - selectedSuggestion: 0, - lastToken: null, - tokenStart: 0, - }; - - onChange = (e) => { - const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); - - if (token !== null && this.state.lastToken !== token) { - this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); - this.props.onSuggestionsFetchRequested(token); - } else if (token === null) { - this.setState({ lastToken: null }); - this.props.onSuggestionsClearRequested(); - } - - this.props.onChange(e); - } - - onKeyDown = (e) => { - const { suggestions, disabled } = this.props; - const { selectedSuggestion, suggestionsHidden } = this.state; - - if (disabled) { - e.preventDefault(); - return; - } - - switch(e.key) { - case 'Escape': - if (!suggestionsHidden) { - e.preventDefault(); - this.setState({ suggestionsHidden: true }); - } - - break; - case 'ArrowDown': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); - } - - break; - case 'ArrowUp': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); - } - - break; - case 'Enter': - case 'Tab': - // Select suggestion - if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - e.stopPropagation(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); - } - - break; - } - - if (e.defaultPrevented || !this.props.onKeyDown) { - return; - } - - this.props.onKeyDown(e); - } - - onKeyUp = e => { - if (e.key === 'Escape' && this.state.suggestionsHidden) { - document.querySelector('.ui').parentElement.focus(); - } - - if (this.props.onKeyUp) { - this.props.onKeyUp(e); - } - } - - onBlur = () => { - this.setState({ suggestionsHidden: true }); - } - - onSuggestionClick = (e) => { - const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); - e.preventDefault(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); - this.textarea.focus(); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { - this.setState({ suggestionsHidden: false }); - } - } - - setTextarea = (c) => { - this.textarea = c; - } - - onPaste = (e) => { - if (e.clipboardData && e.clipboardData.files.length === 1) { - this.props.onPaste(e.clipboardData.files); - e.preventDefault(); - } - } - - renderSuggestion = (suggestion, i) => { - const { selectedSuggestion } = this.state; - let inner, key; - - if (typeof suggestion === 'object') { - inner = <AutosuggestEmoji emoji={suggestion} />; - key = suggestion.id; - } else { - inner = <AutosuggestAccountContainer id={suggestion} />; - key = suggestion; - } - - return ( - <div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}> - {inner} - </div> - ); - } - - render () { - const { value, suggestions, disabled, placeholder, autoFocus } = this.props; - const { suggestionsHidden } = this.state; - const style = { direction: 'ltr' }; - - if (isRtl(value)) { - style.direction = 'rtl'; - } - - return ( - <div className='autosuggest-textarea'> - <label> - <span style={{ display: 'none' }}>{placeholder}</span> - - <Textarea - inputRef={this.setTextarea} - className='autosuggest-textarea__textarea' - disabled={disabled} - placeholder={placeholder} - autoFocus={autoFocus} - value={value} - onChange={this.onChange} - onKeyDown={this.onKeyDown} - onKeyUp={this.onKeyUp} - onBlur={this.onBlur} - onPaste={this.onPaste} - style={style} - /> - </label> - - <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> - {suggestions.map(this.renderSuggestion)} - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/avatar.js b/app/javascript/themes/glitch/components/avatar.js deleted file mode 100644 index dd155f059..000000000 --- a/app/javascript/themes/glitch/components/avatar.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class Avatar extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - size: PropTypes.number.isRequired, - style: PropTypes.object, - animate: PropTypes.bool, - inline: PropTypes.bool, - }; - - static defaultProps = { - animate: false, - size: 20, - inline: false, - }; - - state = { - hovering: false, - }; - - handleMouseEnter = () => { - if (this.props.animate) return; - this.setState({ hovering: true }); - } - - handleMouseLeave = () => { - if (this.props.animate) return; - this.setState({ hovering: false }); - } - - render () { - const { account, size, animate, inline } = this.props; - const { hovering } = this.state; - - const src = account.get('avatar'); - const staticSrc = account.get('avatar_static'); - - let className = 'account__avatar'; - - if (inline) { - className = className + ' account__avatar-inline'; - } - - const style = { - ...this.props.style, - width: `${size}px`, - height: `${size}px`, - backgroundSize: `${size}px ${size}px`, - }; - - if (hovering || animate) { - style.backgroundImage = `url(${src})`; - } else { - style.backgroundImage = `url(${staticSrc})`; - } - - return ( - <div - className={className} - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} - style={style} - data-avatar-of={`@${account.get('acct')}`} - /> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/avatar_overlay.js b/app/javascript/themes/glitch/components/avatar_overlay.js deleted file mode 100644 index 2ecf9fa44..000000000 --- a/app/javascript/themes/glitch/components/avatar_overlay.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class AvatarOverlay extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - friend: ImmutablePropTypes.map.isRequired, - }; - - render() { - const { account, friend } = this.props; - - const baseStyle = { - backgroundImage: `url(${account.get('avatar_static')})`, - }; - - const overlayStyle = { - backgroundImage: `url(${friend.get('avatar_static')})`, - }; - - return ( - <div className='account__avatar-overlay'> - <div className='account__avatar-overlay-base' style={baseStyle} data-avatar-of={`@${account.get('acct')}`} /> - <div className='account__avatar-overlay-overlay' style={overlayStyle} data-avatar-of={`@${friend.get('acct')}`} /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/button.js b/app/javascript/themes/glitch/components/button.js deleted file mode 100644 index 16868010c..000000000 --- a/app/javascript/themes/glitch/components/button.js +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class Button extends React.PureComponent { - - static propTypes = { - text: PropTypes.node, - onClick: PropTypes.func, - disabled: PropTypes.bool, - block: PropTypes.bool, - secondary: PropTypes.bool, - size: PropTypes.number, - className: PropTypes.string, - style: PropTypes.object, - children: PropTypes.node, - title: PropTypes.string, - }; - - static defaultProps = { - size: 36, - }; - - handleClick = (e) => { - if (!this.props.disabled) { - this.props.onClick(e); - } - } - - setRef = (c) => { - this.node = c; - } - - focus() { - this.node.focus(); - } - - render () { - let attrs = { - className: classNames('button', this.props.className, { - 'button-secondary': this.props.secondary, - 'button--block': this.props.block, - }), - disabled: this.props.disabled, - onClick: this.handleClick, - ref: this.setRef, - style: { - padding: `0 ${this.props.size / 2.25}px`, - height: `${this.props.size}px`, - lineHeight: `${this.props.size}px`, - ...this.props.style, - }, - }; - - if (this.props.title) attrs.title = this.props.title; - - return ( - <button {...attrs}> - {this.props.text || this.props.children} - </button> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/collapsable.js b/app/javascript/themes/glitch/components/collapsable.js deleted file mode 100644 index 8bc0a54f4..000000000 --- a/app/javascript/themes/glitch/components/collapsable.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import PropTypes from 'prop-types'; - -const Collapsable = ({ fullHeight, isVisible, children }) => ( - <Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}> - {({ opacity, height }) => - <div style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100, display: Math.floor(opacity) === 0 ? 'none' : 'block' }}> - {children} - </div> - } - </Motion> -); - -Collapsable.propTypes = { - fullHeight: PropTypes.number.isRequired, - isVisible: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired, -}; - -export default Collapsable; diff --git a/app/javascript/themes/glitch/components/column.js b/app/javascript/themes/glitch/components/column.js deleted file mode 100644 index adeba9cc1..000000000 --- a/app/javascript/themes/glitch/components/column.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import detectPassiveEvents from 'detect-passive-events'; -import { scrollTop } from 'themes/glitch/util/scroll'; - -export default class Column extends React.PureComponent { - - static propTypes = { - children: PropTypes.node, - extraClasses: PropTypes.string, - name: PropTypes.string, - }; - - scrollTop () { - const scrollable = this.node.querySelector('.scrollable'); - - if (!scrollable) { - return; - } - - this._interruptScrollAnimation = scrollTop(scrollable); - } - - handleWheel = () => { - if (typeof this._interruptScrollAnimation !== 'function') { - return; - } - - this._interruptScrollAnimation(); - } - - setRef = c => { - this.node = c; - } - - componentDidMount () { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - - componentWillUnmount () { - this.node.removeEventListener('wheel', this.handleWheel); - } - - render () { - const { children, extraClasses, name } = this.props; - - return ( - <div role='region' data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}> - {children} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/column_back_button.js b/app/javascript/themes/glitch/components/column_back_button.js deleted file mode 100644 index 50c3bf11f..000000000 --- a/app/javascript/themes/glitch/components/column_back_button.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class ColumnBackButton extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - handleClick = () => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - } - - render () { - return ( - <button onClick={this.handleClick} className='column-back-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> - <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </button> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/column_back_button_slim.js b/app/javascript/themes/glitch/components/column_back_button_slim.js deleted file mode 100644 index 2cdf1b25b..000000000 --- a/app/javascript/themes/glitch/components/column_back_button_slim.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class ColumnBackButtonSlim extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - handleClick = () => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - } - - render () { - return ( - <div className='column-back-button--slim'> - <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> - <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/column_header.js b/app/javascript/themes/glitch/components/column_header.js deleted file mode 100644 index e601082c8..000000000 --- a/app/javascript/themes/glitch/components/column_header.js +++ /dev/null @@ -1,214 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -// Glitch imports -import NotificationPurgeButtonsContainer from 'themes/glitch/containers/notification_purge_buttons_container'; - -const messages = defineMessages({ - show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, - hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, - moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, - moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, - enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, -}); - -@injectIntl -export default class ColumnHeader extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - intl: PropTypes.object.isRequired, - title: PropTypes.node.isRequired, - icon: PropTypes.string.isRequired, - active: PropTypes.bool, - localSettings : ImmutablePropTypes.map, - multiColumn: PropTypes.bool, - focusable: PropTypes.bool, - showBackButton: PropTypes.bool, - notifCleaning: PropTypes.bool, // true only for the notification column - notifCleaningActive: PropTypes.bool, - onEnterCleaningMode: PropTypes.func, - children: PropTypes.node, - pinned: PropTypes.bool, - onPin: PropTypes.func, - onMove: PropTypes.func, - onClick: PropTypes.func, - intl: PropTypes.object.isRequired, - }; - - static defaultProps = { - focusable: true, - } - - state = { - collapsed: true, - animating: false, - animatingNCD: false, - }; - - handleToggleClick = (e) => { - e.stopPropagation(); - this.setState({ collapsed: !this.state.collapsed, animating: true }); - } - - handleTitleClick = () => { - this.props.onClick(); - } - - handleMoveLeft = () => { - this.props.onMove(-1); - } - - handleMoveRight = () => { - this.props.onMove(1); - } - - handleBackClick = () => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - } - - handleTransitionEnd = () => { - this.setState({ animating: false }); - } - - handleTransitionEndNCD = () => { - this.setState({ animatingNCD: false }); - } - - onEnterCleaningMode = () => { - this.setState({ animatingNCD: true }); - this.props.onEnterCleaningMode(!this.props.notifCleaningActive); - } - - render () { - const { intl, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props; - const { collapsed, animating, animatingNCD } = this.state; - - let title = this.props.title; - - const wrapperClassName = classNames('column-header__wrapper', { - 'active': active, - }); - - const buttonClassName = classNames('column-header', { - 'active': active, - }); - - const collapsibleClassName = classNames('column-header__collapsible', { - 'collapsed': collapsed, - 'animating': animating, - }); - - const collapsibleButtonClassName = classNames('column-header__button', { - 'active': !collapsed, - }); - - const notifCleaningButtonClassName = classNames('column-header__button', { - 'active': notifCleaningActive, - }); - - const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', { - 'collapsed': !notifCleaningActive, - 'animating': animatingNCD, - }); - - let extraContent, pinButton, moveButtons, backButton, collapseButton; - - //*glitch - const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning); - - if (children) { - extraContent = ( - <div key='extra-content' className='column-header__collapsible__extra'> - {children} - </div> - ); - } - - if (multiColumn && pinned) { - pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; - - moveButtons = ( - <div key='move-buttons' className='column-header__setting-arrows'> - <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button> - <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button> - </div> - ); - } else if (multiColumn) { - pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; - } - - if (!pinned && (multiColumn || showBackButton)) { - backButton = ( - <button onClick={this.handleBackClick} className='column-header__back-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> - <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </button> - ); - } - - const collapsedContent = [ - extraContent, - ]; - - if (multiColumn) { - collapsedContent.push(moveButtons); - collapsedContent.push(pinButton); - } - - if (children || multiColumn) { - collapseButton = <button className={collapsibleButtonClassName} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; - } - - return ( - <div className={wrapperClassName}> - <h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}> - <i className={`fa fa-fw fa-${icon} column-header__icon`} /> - <span className='column-header__title'> - {title} - </span> - <div className='column-header__buttons'> - {backButton} - { notifCleaning ? ( - <button - aria-label={msgEnterNotifCleaning} - title={msgEnterNotifCleaning} - onClick={this.onEnterCleaningMode} - className={notifCleaningButtonClassName} - > - <i className='fa fa-eraser' /> - </button> - ) : null} - {collapseButton} - </div> - </h1> - - { notifCleaning ? ( - <div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}> - <div className='column-header__collapsible-inner nopad-drawer'> - {(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null } - </div> - </div> - ) : null} - - <div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}> - <div className='column-header__collapsible-inner'> - {(!collapsed || animating) && collapsedContent} - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/display_name.js b/app/javascript/themes/glitch/components/display_name.js deleted file mode 100644 index 2cf84f8f4..000000000 --- a/app/javascript/themes/glitch/components/display_name.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class DisplayName extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - }; - - render () { - const displayNameHtml = { __html: this.props.account.get('display_name_html') }; - - return ( - <span className='display-name'> - <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span> - </span> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/dropdown_menu.js b/app/javascript/themes/glitch/components/dropdown_menu.js deleted file mode 100644 index d30dc2aaf..000000000 --- a/app/javascript/themes/glitch/components/dropdown_menu.js +++ /dev/null @@ -1,211 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import IconButton from './icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import detectPassiveEvents from 'detect-passive-events'; - -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; - -class DropdownMenu extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - items: PropTypes.array.isRequired, - onClose: PropTypes.func.isRequired, - style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, - }; - - static defaultProps = { - style: {}, - placement: 'bottom', - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - handleClick = e => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; - - this.props.onClose(); - - if (typeof action === 'function') { - e.preventDefault(); - action(); - } else if (to) { - e.preventDefault(); - this.context.router.history.push(to); - } - } - - renderItem (option, i) { - if (option === null) { - return <li key={`sep-${i}`} className='dropdown-menu__separator' />; - } - - const { text, href = '#' } = option; - - return ( - <li className='dropdown-menu__item' key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i}> - {text} - </a> - </li> - ); - } - - render () { - const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props; - - return ( - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}> - <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} /> - - <ul> - {items.map((option, i) => this.renderItem(option, i))} - </ul> - </div> - )} - </Motion> - ); - } - -} - -export default class Dropdown extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - icon: PropTypes.string.isRequired, - items: PropTypes.array.isRequired, - size: PropTypes.number.isRequired, - ariaLabel: PropTypes.string, - disabled: PropTypes.bool, - status: ImmutablePropTypes.map, - isUserTouching: PropTypes.func, - isModalOpen: PropTypes.bool.isRequired, - onModalOpen: PropTypes.func, - onModalClose: PropTypes.func, - }; - - static defaultProps = { - ariaLabel: 'Menu', - }; - - state = { - expanded: false, - }; - - handleClick = () => { - if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) { - const { status, items } = this.props; - - this.props.onModalOpen({ - status, - actions: items, - onClick: this.handleItemClick, - }); - - return; - } - - this.setState({ expanded: !this.state.expanded }); - } - - handleClose = () => { - if (this.props.onModalClose) { - this.props.onModalClose(); - } - - this.setState({ expanded: false }); - } - - handleKeyDown = e => { - switch(e.key) { - case 'Enter': - this.handleClick(); - break; - case 'Escape': - this.handleClose(); - break; - } - } - - handleItemClick = e => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; - - this.handleClose(); - - if (typeof action === 'function') { - e.preventDefault(); - action(); - } else if (to) { - e.preventDefault(); - this.context.router.history.push(to); - } - } - - setTargetRef = c => { - this.target = c; - } - - findTarget = () => { - return this.target; - } - - render () { - const { icon, items, size, ariaLabel, disabled } = this.props; - const { expanded } = this.state; - - return ( - <div onKeyDown={this.handleKeyDown}> - <IconButton - icon={icon} - title={ariaLabel} - active={expanded} - disabled={disabled} - size={size} - ref={this.setTargetRef} - onClick={this.handleClick} - /> - - <Overlay show={expanded} placement='bottom' target={this.findTarget}> - <DropdownMenu items={items} onClose={this.handleClose} /> - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/extended_video_player.js b/app/javascript/themes/glitch/components/extended_video_player.js deleted file mode 100644 index f8bd067e8..000000000 --- a/app/javascript/themes/glitch/components/extended_video_player.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class ExtendedVideoPlayer extends React.PureComponent { - - static propTypes = { - src: PropTypes.string.isRequired, - alt: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - time: PropTypes.number, - controls: PropTypes.bool.isRequired, - muted: PropTypes.bool.isRequired, - }; - - handleLoadedData = () => { - if (this.props.time) { - this.video.currentTime = this.props.time; - } - } - - componentDidMount () { - this.video.addEventListener('loadeddata', this.handleLoadedData); - } - - componentWillUnmount () { - this.video.removeEventListener('loadeddata', this.handleLoadedData); - } - - setRef = (c) => { - this.video = c; - } - - render () { - const { src, muted, controls, alt } = this.props; - - return ( - <div className='extended-video-player'> - <video - ref={this.setRef} - src={src} - autoPlay - role='button' - tabIndex='0' - aria-label={alt} - muted={muted} - controls={controls} - loop={!controls} - /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/icon_button.js b/app/javascript/themes/glitch/components/icon_button.js deleted file mode 100644 index 31cdf4703..000000000 --- a/app/javascript/themes/glitch/components/icon_button.js +++ /dev/null @@ -1,137 +0,0 @@ -import React from 'react'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class IconButton extends React.PureComponent { - - static propTypes = { - className: PropTypes.string, - title: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - onClick: 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, - flip: PropTypes.bool, - overlay: PropTypes.bool, - tabIndex: PropTypes.string, - label: PropTypes.string, - }; - - static defaultProps = { - size: 18, - active: false, - disabled: false, - animate: false, - overlay: false, - tabIndex: '0', - }; - - handleClick = (e) => { - e.preventDefault(); - - if (!this.props.disabled) { - this.props.onClick(e); - } - } - - render () { - let style = { - fontSize: `${this.props.size}px`, - height: `${this.props.size * 1.28571429}px`, - lineHeight: `${this.props.size}px`, - ...this.props.style, - ...(this.props.active ? this.props.activeStyle : {}), - }; - if (!this.props.label) { - style.width = `${this.props.size * 1.28571429}px`; - } else { - style.textAlign = 'left'; - } - - const { - active, - animate, - className, - disabled, - expanded, - icon, - inverted, - flip, - overlay, - pressed, - tabIndex, - title, - } = this.props; - - const classes = classNames(className, 'icon-button', { - active, - disabled, - inverted, - overlayed: overlay, - }); - - const flipDeg = flip ? -180 : -360; - const rotateDeg = active ? flipDeg : 0; - - const motionDefaultStyle = { - rotate: rotateDeg, - }; - - const springOpts = { - stiffness: this.props.flip ? 60 : 120, - damping: 7, - }; - const motionStyle = { - rotate: animate ? spring(rotateDeg, springOpts) : 0, - }; - - if (!animate) { - // Perf optimization: avoid unnecessary <Motion> components unless - // we actually need to animate. - return ( - <button - aria-label={title} - aria-pressed={pressed} - aria-expanded={expanded} - title={title} - className={classes} - onClick={this.handleClick} - style={style} - tabIndex={tabIndex} - > - <i className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> - </button> - ); - } - - return ( - <Motion defaultStyle={motionDefaultStyle} style={motionStyle}> - {({ rotate }) => - <button - aria-label={title} - aria-pressed={pressed} - aria-expanded={expanded} - title={title} - className={classes} - onClick={this.handleClick} - style={style} - tabIndex={tabIndex} - > - <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> - {this.props.label} - </button> - } - </Motion> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/intersection_observer_article.js b/app/javascript/themes/glitch/components/intersection_observer_article.js deleted file mode 100644 index f0139ac75..000000000 --- a/app/javascript/themes/glitch/components/intersection_observer_article.js +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import scheduleIdleTask from 'themes/glitch/util/schedule_idle_task'; -import getRectFromEntry from 'themes/glitch/util/get_rect_from_entry'; -import { is } from 'immutable'; - -// Diff these props in the "rendered" state -const updateOnPropsForRendered = ['id', 'index', 'listLength']; -// Diff these props in the "unrendered" state -const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight']; - -export default class IntersectionObserverArticle extends React.Component { - - static propTypes = { - intersectionObserverWrapper: PropTypes.object.isRequired, - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - saveHeightKey: PropTypes.string, - cachedHeight: PropTypes.number, - onHeightChange: PropTypes.func, - children: PropTypes.node, - }; - - state = { - isHidden: false, // set to true in requestIdleCallback to trigger un-render - } - - shouldComponentUpdate (nextProps, nextState) { - const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight); - const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight); - if (!!isUnrendered !== !!willBeUnrendered) { - // If we're going from rendered to unrendered (or vice versa) then update - return true; - } - // Otherwise, diff based on props - const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered; - return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop])); - } - - componentDidMount () { - const { intersectionObserverWrapper, id } = this.props; - - intersectionObserverWrapper.observe( - id, - this.node, - this.handleIntersection - ); - - this.componentMounted = true; - } - - componentWillUnmount () { - const { intersectionObserverWrapper, id } = this.props; - intersectionObserverWrapper.unobserve(id, this.node); - - this.componentMounted = false; - } - - handleIntersection = (entry) => { - this.entry = entry; - - scheduleIdleTask(this.calculateHeight); - this.setState(this.updateStateAfterIntersection); - } - - updateStateAfterIntersection = (prevState) => { - if (prevState.isIntersecting && !this.entry.isIntersecting) { - scheduleIdleTask(this.hideIfNotIntersecting); - } - return { - isIntersecting: this.entry.isIntersecting, - isHidden: false, - }; - } - - calculateHeight = () => { - const { onHeightChange, saveHeightKey, id } = this.props; - // save the height of the fully-rendered element (this is expensive - // on Chrome, where we need to fall back to getBoundingClientRect) - this.height = getRectFromEntry(this.entry).height; - - if (onHeightChange && saveHeightKey) { - onHeightChange(saveHeightKey, id, this.height); - } - } - - hideIfNotIntersecting = () => { - if (!this.componentMounted) { - return; - } - - // When the browser gets a chance, test if we're still not intersecting, - // and if so, set our isHidden to true to trigger an unrender. The point of - // this is to save DOM nodes and avoid using up too much memory. - // See: https://github.com/tootsuite/mastodon/issues/2900 - this.setState((prevState) => ({ isHidden: !prevState.isIntersecting })); - } - - handleRef = (node) => { - this.node = node; - } - - render () { - const { children, id, index, listLength, cachedHeight } = this.props; - const { isIntersecting, isHidden } = this.state; - - if (!isIntersecting && (isHidden || cachedHeight)) { - return ( - <article - ref={this.handleRef} - aria-posinset={index} - aria-setsize={listLength} - style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }} - data-id={id} - tabIndex='0' - > - {children && React.cloneElement(children, { hidden: true })} - </article> - ); - } - - return ( - <article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'> - {children && React.cloneElement(children, { hidden: false })} - </article> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/load_more.js b/app/javascript/themes/glitch/components/load_more.js deleted file mode 100644 index c4c8c94a2..000000000 --- a/app/javascript/themes/glitch/components/load_more.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class LoadMore extends React.PureComponent { - - static propTypes = { - onClick: PropTypes.func, - visible: PropTypes.bool, - } - - static defaultProps = { - visible: true, - } - - render() { - const { visible } = this.props; - - return ( - <button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}> - <FormattedMessage id='status.load_more' defaultMessage='Load more' /> - </button> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/loading_indicator.js b/app/javascript/themes/glitch/components/loading_indicator.js deleted file mode 100644 index d6a5adb6f..000000000 --- a/app/javascript/themes/glitch/components/loading_indicator.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -const LoadingIndicator = () => ( - <div className='loading-indicator'> - <div className='loading-indicator__figure' /> - <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' /> - </div> -); - -export default LoadingIndicator; diff --git a/app/javascript/themes/glitch/components/media_gallery.js b/app/javascript/themes/glitch/components/media_gallery.js deleted file mode 100644 index b6b40c585..000000000 --- a/app/javascript/themes/glitch/components/media_gallery.js +++ /dev/null @@ -1,255 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { is } from 'immutable'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { isIOS } from 'themes/glitch/util/is_mobile'; -import classNames from 'classnames'; -import { autoPlayGif } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, -}); - -class Item extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - attachment: ImmutablePropTypes.map.isRequired, - standalone: PropTypes.bool, - index: PropTypes.number.isRequired, - size: PropTypes.number.isRequired, - letterbox: PropTypes.bool, - onClick: PropTypes.func.isRequired, - }; - - static defaultProps = { - standalone: false, - index: 0, - size: 1, - }; - - handleMouseEnter = (e) => { - if (this.hoverToPlay()) { - e.target.play(); - } - } - - handleMouseLeave = (e) => { - if (this.hoverToPlay()) { - e.target.pause(); - e.target.currentTime = 0; - } - } - - hoverToPlay () { - const { attachment } = this.props; - return !autoPlayGif && attachment.get('type') === 'gifv'; - } - - handleClick = (e) => { - const { index, onClick } = this.props; - - if (this.context.router && e.button === 0) { - e.preventDefault(); - onClick(index); - } - - e.stopPropagation(); - } - - render () { - const { attachment, index, size, standalone, letterbox } = this.props; - - let width = 50; - let height = 100; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; - - if (size === 1) { - width = 100; - } - - if (size === 4 || (size === 3 && index > 0)) { - height = 50; - } - - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } - } - - let thumbnail = ''; - - if (attachment.get('type') === 'image') { - const previewUrl = attachment.get('preview_url'); - const previewWidth = attachment.getIn(['meta', 'small', 'width']); - - const originalUrl = attachment.get('url'); - const originalWidth = attachment.getIn(['meta', 'original', 'width']); - - const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; - - const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null; - const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null; - - thumbnail = ( - <a - className='media-gallery__item-thumbnail' - href={attachment.get('remote_url') || originalUrl} - onClick={this.handleClick} - target='_blank' - > - <img className={letterbox ? 'letterbox' : null} src={previewUrl} srcSet={srcSet} sizes={sizes} alt={attachment.get('description')} title={attachment.get('description')} /> - </a> - ); - } else if (attachment.get('type') === 'gifv') { - const autoPlay = !isIOS() && autoPlayGif; - - thumbnail = ( - <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> - <video - className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`} - aria-label={attachment.get('description')} - role='application' - src={attachment.get('url')} - onClick={this.handleClick} - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} - autoPlay={autoPlay} - loop - muted - /> - - <span className='media-gallery__gifv__label'>GIF</span> - </div> - ); - } - - return ( - <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - {thumbnail} - </div> - ); - } - -} - -@injectIntl -export default class MediaGallery extends React.PureComponent { - - static propTypes = { - sensitive: PropTypes.bool, - standalone: PropTypes.bool, - letterbox: PropTypes.bool, - fullwidth: PropTypes.bool, - media: ImmutablePropTypes.list.isRequired, - size: PropTypes.object, - onOpenMedia: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - static defaultProps = { - standalone: false, - }; - - state = { - visible: !this.props.sensitive, - }; - - componentWillReceiveProps (nextProps) { - if (!is(nextProps.media, this.props.media)) { - this.setState({ visible: !nextProps.sensitive }); - } - } - - handleOpen = () => { - this.setState({ visible: !this.state.visible }); - } - - handleClick = (index) => { - this.props.onOpenMedia(this.props.media, index); - } - - isStandaloneEligible() { - const { media, standalone } = this.props; - return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']); - } - - render () { - const { media, intl, sensitive, letterbox, fullwidth } = this.props; - const { visible } = this.state; - const size = media.take(4).size; - - let children; - - if (!visible) { - let warning; - - if (sensitive) { - warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; - } else { - warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; - } - - children = ( - <button className='media-spoiler' onClick={this.handleOpen}> - <span className='media-spoiler__warning'>{warning}</span> - <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </button> - ); - } else { - if (this.isStandaloneEligible()) { - children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />; - } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} />); - } - } - - return ( - <div className={`media-gallery size-${size} ${fullwidth ? 'full-width' : ''}`}> - <div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}> - <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> - </div> - - {children} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/missing_indicator.js b/app/javascript/themes/glitch/components/missing_indicator.js deleted file mode 100644 index 87df7f61c..000000000 --- a/app/javascript/themes/glitch/components/missing_indicator.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -const MissingIndicator = () => ( - <div className='missing-indicator'> - <div> - <FormattedMessage id='missing_indicator.label' defaultMessage='Not found' /> - </div> - </div> -); - -export default MissingIndicator; diff --git a/app/javascript/themes/glitch/components/notification_purge_buttons.js b/app/javascript/themes/glitch/components/notification_purge_buttons.js deleted file mode 100644 index e0c1543b0..000000000 --- a/app/javascript/themes/glitch/components/notification_purge_buttons.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Buttons widget for controlling the notification clearing mode. - * In idle state, the cleaning mode button is shown. When the mode is active, - * a Confirm and Abort buttons are shown in its place. - */ - - -// Package imports // -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' }, - btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' }, - btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' }, - btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' }, -}); - -@injectIntl -export default class NotificationPurgeButtons extends ImmutablePureComponent { - - static propTypes = { - onDeleteMarked : PropTypes.func.isRequired, - onMarkAll : PropTypes.func.isRequired, - onMarkNone : PropTypes.func.isRequired, - onInvert : PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - markNewForDelete: PropTypes.bool, - }; - - render () { - const { intl, markNewForDelete } = this.props; - - //className='active' - return ( - <div className='column-header__notif-cleaning-buttons'> - <button onClick={this.props.onMarkAll} className={markNewForDelete ? 'active' : ''}> - <b>∀</b><br />{intl.formatMessage(messages.btnAll)} - </button> - - <button onClick={this.props.onMarkNone} className={!markNewForDelete ? 'active' : ''}> - <b>∅</b><br />{intl.formatMessage(messages.btnNone)} - </button> - - <button onClick={this.props.onInvert}> - <b>¬</b><br />{intl.formatMessage(messages.btnInvert)} - </button> - - <button onClick={this.props.onDeleteMarked}> - <i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)} - </button> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/permalink.js b/app/javascript/themes/glitch/components/permalink.js deleted file mode 100644 index d726d37a2..000000000 --- a/app/javascript/themes/glitch/components/permalink.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class Permalink extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - className: PropTypes.string, - href: PropTypes.string.isRequired, - to: PropTypes.string.isRequired, - children: PropTypes.node, - }; - - handleClick = (e) => { - if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.context.router.history.push(this.props.to); - } - } - - render () { - const { href, children, className, ...other } = this.props; - - return ( - <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}> - {children} - </a> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/relative_timestamp.js b/app/javascript/themes/glitch/components/relative_timestamp.js deleted file mode 100644 index 51588e78c..000000000 --- a/app/javascript/themes/glitch/components/relative_timestamp.js +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import { injectIntl, defineMessages } from 'react-intl'; -import PropTypes from 'prop-types'; - -const messages = defineMessages({ - just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, - seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, - minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, - hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, - days: { id: 'relative_time.days', defaultMessage: '{number}d' }, -}); - -const dateFormatOptions = { - hour12: false, - year: 'numeric', - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', -}; - -const shortDateFormatOptions = { - month: 'numeric', - day: 'numeric', -}; - -const SECOND = 1000; -const MINUTE = 1000 * 60; -const HOUR = 1000 * 60 * 60; -const DAY = 1000 * 60 * 60 * 24; - -const MAX_DELAY = 2147483647; - -const selectUnits = delta => { - const absDelta = Math.abs(delta); - - if (absDelta < MINUTE) { - return 'second'; - } else if (absDelta < HOUR) { - return 'minute'; - } else if (absDelta < DAY) { - return 'hour'; - } - - return 'day'; -}; - -const getUnitDelay = units => { - switch (units) { - case 'second': - return SECOND; - case 'minute': - return MINUTE; - case 'hour': - return HOUR; - case 'day': - return DAY; - default: - return MAX_DELAY; - } -}; - -@injectIntl -export default class RelativeTimestamp extends React.Component { - - static propTypes = { - intl: PropTypes.object.isRequired, - timestamp: PropTypes.string.isRequired, - }; - - state = { - now: this.props.intl.now(), - }; - - shouldComponentUpdate (nextProps, nextState) { - // As of right now the locale doesn't change without a new page load, - // but we might as well check in case that ever changes. - return this.props.timestamp !== nextProps.timestamp || - this.props.intl.locale !== nextProps.intl.locale || - this.state.now !== nextState.now; - } - - componentWillReceiveProps (nextProps) { - if (this.props.timestamp !== nextProps.timestamp) { - this.setState({ now: this.props.intl.now() }); - } - } - - componentDidMount () { - this._scheduleNextUpdate(this.props, this.state); - } - - componentWillUpdate (nextProps, nextState) { - this._scheduleNextUpdate(nextProps, nextState); - } - - componentWillUnmount () { - clearTimeout(this._timer); - } - - _scheduleNextUpdate (props, state) { - clearTimeout(this._timer); - - const { timestamp } = props; - const delta = (new Date(timestamp)).getTime() - state.now; - const unitDelay = getUnitDelay(selectUnits(delta)); - const unitRemainder = Math.abs(delta % unitDelay); - const updateInterval = 1000 * 10; - const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder); - - this._timer = setTimeout(() => { - this.setState({ now: this.props.intl.now() }); - }, delay); - } - - render () { - const { timestamp, intl } = this.props; - - const date = new Date(timestamp); - const delta = this.state.now - date.getTime(); - - let relativeTime; - - if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(messages.just_now); - } else if (delta < 3 * DAY) { - if (delta < MINUTE) { - relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) }); - } else if (delta < HOUR) { - relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) }); - } else if (delta < DAY) { - relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) }); - } else { - relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) }); - } - } else { - relativeTime = intl.formatDate(date, shortDateFormatOptions); - } - - return ( - <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}> - {relativeTime} - </time> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/scrollable_list.js b/app/javascript/themes/glitch/components/scrollable_list.js deleted file mode 100644 index ccdcd7c85..000000000 --- a/app/javascript/themes/glitch/components/scrollable_list.js +++ /dev/null @@ -1,198 +0,0 @@ -import React, { PureComponent } from 'react'; -import { ScrollContainer } from 'react-router-scroll-4'; -import PropTypes from 'prop-types'; -import IntersectionObserverArticleContainer from 'themes/glitch/containers/intersection_observer_article_container'; -import LoadMore from './load_more'; -import IntersectionObserverWrapper from 'themes/glitch/util/intersection_observer_wrapper'; -import { throttle } from 'lodash'; -import { List as ImmutableList } from 'immutable'; -import classNames from 'classnames'; -import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'themes/glitch/util/fullscreen'; - -export default class ScrollableList extends PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - scrollKey: PropTypes.string.isRequired, - onScrollToBottom: PropTypes.func, - onScrollToTop: PropTypes.func, - onScroll: PropTypes.func, - trackScroll: PropTypes.bool, - shouldUpdateScroll: PropTypes.func, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - prepend: PropTypes.node, - emptyMessage: PropTypes.node, - children: PropTypes.node, - }; - - static defaultProps = { - trackScroll: true, - }; - - state = { - lastMouseMove: null, - }; - - intersectionObserverWrapper = new IntersectionObserverWrapper(); - - handleScroll = throttle(() => { - if (this.node) { - const { scrollTop, scrollHeight, clientHeight } = this.node; - const offset = scrollHeight - scrollTop - clientHeight; - this._oldScrollPosition = scrollHeight - scrollTop; - - if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) { - this.props.onScrollToBottom(); - } else if (scrollTop < 100 && this.props.onScrollToTop) { - this.props.onScrollToTop(); - } else if (this.props.onScroll) { - this.props.onScroll(); - } - } - }, 150, { - trailing: true, - }); - - handleMouseMove = throttle(() => { - this._lastMouseMove = new Date(); - }, 300); - - handleMouseLeave = () => { - this._lastMouseMove = null; - } - - componentDidMount () { - this.attachScrollListener(); - this.attachIntersectionObserver(); - attachFullscreenListener(this.onFullScreenChange); - - // Handle initial scroll posiiton - this.handleScroll(); - } - - componentDidUpdate (prevProps) { - const someItemInserted = React.Children.count(prevProps.children) > 0 && - React.Children.count(prevProps.children) < React.Children.count(this.props.children) && - this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); - - // Reset the scroll position when a new child comes in in order not to - // jerk the scrollbar around if you're already scrolled down the page. - if (someItemInserted && this._oldScrollPosition && this.node.scrollTop > 0) { - const newScrollTop = this.node.scrollHeight - this._oldScrollPosition; - - if (this.node.scrollTop !== newScrollTop) { - this.node.scrollTop = newScrollTop; - } - } else { - this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop; - } - } - - componentWillUnmount () { - this.detachScrollListener(); - this.detachIntersectionObserver(); - detachFullscreenListener(this.onFullScreenChange); - } - - onFullScreenChange = () => { - this.setState({ fullscreen: isFullscreen() }); - } - - attachIntersectionObserver () { - this.intersectionObserverWrapper.connect({ - root: this.node, - rootMargin: '300% 0px', - }); - } - - detachIntersectionObserver () { - this.intersectionObserverWrapper.disconnect(); - } - - attachScrollListener () { - this.node.addEventListener('scroll', this.handleScroll); - } - - detachScrollListener () { - this.node.removeEventListener('scroll', this.handleScroll); - } - - getFirstChildKey (props) { - const { children } = props; - let firstChild = children; - if (children instanceof ImmutableList) { - firstChild = children.get(0); - } else if (Array.isArray(children)) { - firstChild = children[0]; - } - return firstChild && firstChild.key; - } - - setRef = (c) => { - this.node = c; - } - - handleLoadMore = (e) => { - e.preventDefault(); - this.props.onScrollToBottom(); - } - - _recentlyMoved () { - return this._lastMouseMove !== null && ((new Date()) - this._lastMouseMove < 600); - } - - render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; - const { fullscreen } = this.state; - const childrenCount = React.Children.count(children); - - const loadMore = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null; - let scrollableArea = null; - - if (isLoading || childrenCount > 0 || !emptyMessage) { - scrollableArea = ( - <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}> - <div role='feed' className='item-list'> - {prepend} - - {React.Children.map(this.props.children, (child, index) => ( - <IntersectionObserverArticleContainer - key={child.key} - id={child.key} - index={index} - listLength={childrenCount} - intersectionObserverWrapper={this.intersectionObserverWrapper} - saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null} - > - {child} - </IntersectionObserverArticleContainer> - ))} - - {loadMore} - </div> - </div> - ); - } else { - scrollableArea = ( - <div className='empty-column-indicator' ref={this.setRef}> - {emptyMessage} - </div> - ); - } - - if (trackScroll) { - return ( - <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}> - {scrollableArea} - </ScrollContainer> - ); - } else { - return scrollableArea; - } - } - -} diff --git a/app/javascript/themes/glitch/components/setting_text.js b/app/javascript/themes/glitch/components/setting_text.js deleted file mode 100644 index a6dde4c0f..000000000 --- a/app/javascript/themes/glitch/components/setting_text.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class SettingText extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - settingKey: PropTypes.array.isRequired, - label: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - }; - - handleChange = (e) => { - this.props.onChange(this.props.settingKey, e.target.value); - } - - render () { - const { settings, settingKey, label } = this.props; - - return ( - <label> - <span style={{ display: 'none' }}>{label}</span> - <input - className='setting-text' - value={settings.getIn(settingKey)} - onChange={this.handleChange} - placeholder={label} - /> - </label> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status.js b/app/javascript/themes/glitch/components/status.js deleted file mode 100644 index 9288bcafa..000000000 --- a/app/javascript/themes/glitch/components/status.js +++ /dev/null @@ -1,440 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import StatusPrepend from './status_prepend'; -import StatusHeader from './status_header'; -import StatusContent from './status_content'; -import StatusActionBar from './status_action_bar'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video } from 'themes/glitch/util/async-components'; -import { HotKeys } from 'react-hotkeys'; -import NotificationOverlayContainer from 'themes/glitch/features/notifications/containers/overlay_container'; -import classNames from 'classnames'; - -// 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'; - -export default class Status extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - containerId: PropTypes.string, - id: PropTypes.string, - status: ImmutablePropTypes.map, - account: ImmutablePropTypes.map, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onReblog: PropTypes.func, - onDelete: PropTypes.func, - onPin: PropTypes.func, - onOpenMedia: PropTypes.func, - onOpenVideo: PropTypes.func, - onBlock: PropTypes.func, - onEmbed: PropTypes.func, - onHeightChange: PropTypes.func, - muted: PropTypes.bool, - collapse: PropTypes.bool, - hidden: PropTypes.bool, - prepend: PropTypes.string, - withDismiss: PropTypes.bool, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - }; - - state = { - isExpanded: null, - markedForDelete: false, - } - - // Avoid checking props that are functions (and whose equality will always - // evaluate to false. See react-immutable-pure-component for usage. - updateOnProps = [ - 'status', - 'account', - 'settings', - 'prepend', - 'boostModal', - 'muted', - 'collapse', - 'notification', - 'hidden', - ] - - updateOnStates = [ - 'isExpanded', - 'markedForDelete', - ] - - // If our settings have changed to disable collapsed statuses, then we - // need to make sure that we uncollapse every one. We do that by watching - // for changes to `settings.collapsed.enabled` in - // `componentWillReceiveProps()`. - - // We also need to watch for changes on the `collapse` prop---if this - // changes to anything other than `undefined`, then we need to collapse or - // uncollapse our status accordingly. - componentWillReceiveProps (nextProps) { - if (!nextProps.settings.getIn(['collapsed', 'enabled'])) { - if (this.state.isExpanded === false) { - this.setExpansion(null); - } - } else if ( - nextProps.collapse !== this.props.collapse && - nextProps.collapse !== undefined - ) this.setExpansion(nextProps.collapse ? false : null); - } - - // When mounting, we just check to see if our status should be collapsed, - // and collapse it if so. We don't need to worry about whether collapsing - // is enabled here, because `setExpansion()` already takes that into - // account. - - // The cases where a status should be collapsed are: - // - // - The `collapse` prop has been set to `true` - // - The user has decided in local settings to collapse all statuses. - // - The user has decided to collapse all notifications ('muted' - // statuses). - // - The user has decided to collapse long statuses and the status is - // over 400px (without media, or 650px with). - // - The status is a reply and the user has decided to collapse all - // replies. - // - The status contains media and the user has decided to collapse all - // statuses with media. - // - The status is a reblog the user has decided to collapse all - // statuses which are reblogs. - componentDidMount () { - const { node } = this; - const { - status, - settings, - collapse, - muted, - prepend, - } = this.props; - const autoCollapseSettings = settings.getIn(['collapsed', 'auto']); - - if (function () { - switch (true) { - case collapse: - case autoCollapseSettings.get('all'): - case autoCollapseSettings.get('notifications') && muted: - case autoCollapseSettings.get('lengthy') && node.clientHeight > ( - status.get('media_attachments').size && !muted ? 650 : 400 - ): - case autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by': - case autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null: - case autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && status.get('media_attachments').size: - return true; - default: - return false; - } - }()) this.setExpansion(false); - } - - // `setExpansion()` sets the value of `isExpanded` in our state. It takes - // one argument, `value`, which gives the desired value for `isExpanded`. - // The default for this argument is `null`. - - // `setExpansion()` automatically checks for us whether toot collapsing - // is enabled, so we don't have to. - setExpansion = (value) => { - switch (true) { - case value === undefined || value === null: - this.setState({ isExpanded: null }); - break; - case !value && this.props.settings.getIn(['collapsed', 'enabled']): - this.setState({ isExpanded: false }); - break; - case !!value: - this.setState({ isExpanded: true }); - break; - } - } - - // `parseClick()` takes a click event and responds appropriately. - // If our status is collapsed, then clicking on it should uncollapse it. - // If `Shift` is held, then clicking on it should collapse it. - // Otherwise, we open the url handed to us in `destination`, if - // applicable. - parseClick = (e, destination) => { - const { router } = this.context; - const { status } = this.props; - const { isExpanded } = this.state; - if (!router) return; - if (destination === undefined) { - destination = `/statuses/${ - status.getIn(['reblog', 'id'], status.get('id')) - }`; - } - if (e.button === 0) { - if (isExpanded === false) this.setExpansion(null); - else if (e.shiftKey) { - this.setExpansion(false); - document.getSelection().removeAllRanges(); - } else router.history.push(destination); - e.preventDefault(); - } - } - - handleAccountClick = (e) => { - if (this.context.router && e.button === 0) { - const id = e.currentTarget.getAttribute('data-id'); - e.preventDefault(); - this.context.router.history.push(`/accounts/${id}`); - } - } - - handleExpandedToggle = () => { - if (this.props.status.get('spoiler_text')) { - this.setExpansion(this.state.isExpanded ? null : true); - } - }; - - handleOpenVideo = startTime => { - this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); - } - - handleHotkeyReply = e => { - e.preventDefault(); - this.props.onReply(this.props.status, this.context.router.history); - } - - handleHotkeyFavourite = () => { - this.props.onFavourite(this.props.status); - } - - handleHotkeyBoost = e => { - this.props.onReblog(this.props.status, e); - } - - handleHotkeyMention = e => { - e.preventDefault(); - this.props.onMention(this.props.status.get('account'), this.context.router.history); - } - - handleHotkeyOpen = () => { - this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); - } - - handleHotkeyOpenProfile = () => { - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - - handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.containerId || this.props.id); - } - - handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.containerId || this.props.id); - } - - handleRef = c => { - this.node = c; - } - - renderLoadingMediaGallery () { - return <div className='media_gallery' style={{ height: '110px' }} />; - } - - renderLoadingVideoPlayer () { - return <div className='media-spoiler-video' style={{ height: '110px' }} />; - } - - render () { - const { - handleRef, - parseClick, - setExpansion, - } = this; - const { router } = this.context; - const { - status, - account, - settings, - collapsed, - muted, - prepend, - intersectionObserverWrapper, - onOpenVideo, - onOpenMedia, - notification, - hidden, - ...other - } = this.props; - const { isExpanded } = this.state; - let background = null; - let attachments = null; - let media = null; - let mediaIcon = null; - - if (status === null) { - return null; - } - - if (hidden) { - return ( - <div - ref={this.handleRef} - data-id={status.get('id')} - style={{ - height: `${this.height}px`, - opacity: 0, - overflow: 'hidden', - }} - > - {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {' '} - {status.get('content')} - </div> - ); - } - - // If user backgrounds for collapsed statuses are enabled, then we - // initialize our background accordingly. This will only be rendered if - // the status is collapsed. - if (settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds'])) { - background = status.getIn(['account', 'header']); - } - - // This handles our media attachments. Note that we don't show media on - // muted (notification) statuses. If the media type is unknown, then we - // simply ignore it. - - // After we have generated our appropriate media element and stored it in - // `media`, we snatch the thumbnail to use as our `background` if media - // backgrounds for collapsed statuses are enabled. - attachments = status.get('media_attachments'); - if (attachments.size > 0 && !muted) { - if (attachments.some(item => item.get('type') === 'unknown')) { // Media type is 'unknown' - /* Do nothing */ - } else if (attachments.getIn([0, 'type']) === 'video') { // Media type is 'video' - const video = status.getIn(['media_attachments', 0]); - - media = ( - <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > - {Component => <Component - preview={video.get('preview_url')} - src={video.get('url')} - sensitive={status.get('sensitive')} - letterbox={settings.getIn(['media', 'letterbox'])} - fullwidth={settings.getIn(['media', 'fullwidth'])} - onOpenVideo={this.handleOpenVideo} - />} - </Bundle> - ); - mediaIcon = 'video-camera'; - } else { // Media type is 'image' or 'gifv' - media = ( - <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} > - {Component => ( - <Component - media={attachments} - sensitive={status.get('sensitive')} - letterbox={settings.getIn(['media', 'letterbox'])} - fullwidth={settings.getIn(['media', 'fullwidth'])} - onOpenMedia={this.props.onOpenMedia} - /> - )} - </Bundle> - ); - mediaIcon = 'picture-o'; - } - - if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) { - background = attachments.getIn([0, 'preview_url']); - } - } - - // Here we prepare extra data-* attributes for CSS selectors. - // Users can use those for theming, hiding avatars etc via UserStyle - const selectorAttribs = { - 'data-status-by': `@${status.getIn(['account', 'acct'])}`, - }; - - if (prepend && account) { - const notifKind = { - favourite: 'favourited', - reblog: 'boosted', - reblogged_by: 'boosted', - }[prepend]; - - selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`; - } - - const handlers = { - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - open: this.handleHotkeyOpen, - openProfile: this.handleHotkeyOpenProfile, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleSpoiler: this.handleExpandedToggle, - }; - - const computedClass = classNames('status', `status-${status.get('visibility')}`, { - collapsed: isExpanded === false, - 'has-background': isExpanded === false && background, - 'marked-for-delete': this.state.markedForDelete, - muted, - }, 'focusable'); - - return ( - <HotKeys handlers={handlers}> - <div - className={computedClass} - style={isExpanded === false && background ? { backgroundImage: `url(${background})` } : null} - {...selectorAttribs} - ref={handleRef} - tabIndex='0' - > - {prepend && account ? ( - <StatusPrepend - type={prepend} - account={account} - parseClick={parseClick} - notificationId={this.props.notificationId} - /> - ) : null} - <StatusHeader - status={status} - friend={account} - mediaIcon={mediaIcon} - collapsible={settings.getIn(['collapsed', 'enabled'])} - collapsed={isExpanded === false} - parseClick={parseClick} - setExpansion={setExpansion} - /> - <StatusContent - status={status} - media={media} - mediaIcon={mediaIcon} - expanded={isExpanded} - setExpansion={setExpansion} - parseClick={parseClick} - disabled={!router} - /> - {isExpanded !== false ? ( - <StatusActionBar - {...other} - status={status} - account={status.get('account')} - /> - ) : null} - {notification ? ( - <NotificationOverlayContainer - notification={notification} - /> - ) : null} - </div> - </HotKeys> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_action_bar.js b/app/javascript/themes/glitch/components/status_action_bar.js deleted file mode 100644 index 9d615ed7c..000000000 --- a/app/javascript/themes/glitch/components/status_action_bar.js +++ /dev/null @@ -1,188 +0,0 @@ -// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! -// SEE INSTEAD : glitch/components/status/action_bar - -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'themes/glitch/util/initial_state'; -import RelativeTimestamp from './relative_timestamp'; - -const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - share: { id: 'status.share', defaultMessage: 'Share' }, - more: { id: 'status.more', defaultMessage: 'More' }, - replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, - cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, - favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, - open: { id: 'status.open', defaultMessage: 'Expand this status' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' }, - muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, - unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, - pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, - unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, - embed: { id: 'status.embed', defaultMessage: 'Embed' }, -}); - -@injectIntl -export default class StatusActionBar extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onReblog: PropTypes.func, - onDelete: PropTypes.func, - onMention: PropTypes.func, - onMute: PropTypes.func, - onBlock: PropTypes.func, - onReport: PropTypes.func, - onEmbed: PropTypes.func, - onMuteConversation: PropTypes.func, - onPin: PropTypes.func, - withDismiss: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - // Avoid checking props that are functions (and whose equality will always - // evaluate to false. See react-immutable-pure-component for usage. - updateOnProps = [ - 'status', - 'withDismiss', - ] - - handleReplyClick = () => { - this.props.onReply(this.props.status, this.context.router.history); - } - - handleShareClick = () => { - navigator.share({ - text: this.props.status.get('search_index'), - url: this.props.status.get('url'), - }); - } - - handleFavouriteClick = () => { - this.props.onFavourite(this.props.status); - } - - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); - } - - handleDeleteClick = () => { - this.props.onDelete(this.props.status); - } - - handlePinClick = () => { - this.props.onPin(this.props.status); - } - - handleMentionClick = () => { - this.props.onMention(this.props.status.get('account'), this.context.router.history); - } - - handleMuteClick = () => { - this.props.onMute(this.props.status.get('account')); - } - - handleBlockClick = () => { - this.props.onBlock(this.props.status.get('account')); - } - - handleOpen = () => { - this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); - } - - handleEmbed = () => { - this.props.onEmbed(this.props.status); - } - - handleReport = () => { - this.props.onReport(this.props.status); - } - - handleConversationMuteClick = () => { - this.props.onMuteConversation(this.props.status); - } - - render () { - const { status, intl, withDismiss } = this.props; - - const mutingConversation = status.get('muted'); - const anonymousAccess = !me; - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - - let menu = []; - let reblogIcon = 'retweet'; - let replyIcon; - let replyTitle; - - menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); - - if (publicStatus) { - menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); - } - - menu.push(null); - - if (status.getIn(['account', 'id']) === me || withDismiss) { - menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - menu.push(null); - } - - if (status.getIn(['account', 'id']) === me) { - if (publicStatus) { - menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - } - - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - } - - if (status.get('in_reply_to_id', null) === null) { - replyIcon = 'reply'; - replyTitle = intl.formatMessage(messages.reply); - } else { - replyIcon = 'reply-all'; - replyTitle = intl.formatMessage(messages.replyAll); - } - - const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( - <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} /> - ); - - return ( - <div className='status__action-bar'> - <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /> - <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> - <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> - {shareButton} - - <div className='status__action-bar-dropdown'> - <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} /> - </div> - - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_content.js b/app/javascript/themes/glitch/components/status_content.js deleted file mode 100644 index 3eba6eaa0..000000000 --- a/app/javascript/themes/glitch/components/status_content.js +++ /dev/null @@ -1,245 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { isRtl } from 'themes/glitch/util/rtl'; -import { FormattedMessage } from 'react-intl'; -import Permalink from './permalink'; -import classnames from 'classnames'; - -export default class StatusContent extends React.PureComponent { - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - expanded: PropTypes.bool, - setExpansion: PropTypes.func, - media: PropTypes.element, - mediaIcon: PropTypes.string, - parseClick: PropTypes.func, - disabled: PropTypes.bool, - }; - - state = { - hidden: true, - }; - - _updateStatusLinks () { - const node = this.node; - const links = node.querySelectorAll('a'); - - for (var i = 0; i < links.length; ++i) { - let link = links[i]; - if (link.classList.contains('status-link')) { - continue; - } - link.classList.add('status-link'); - - let mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); - - if (mention) { - link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', mention.get('acct')); - } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { - link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); - } else { - link.addEventListener('click', this.onLinkClick.bind(this), false); - link.setAttribute('title', link.href); - } - - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); - } - } - - componentDidMount () { - this._updateStatusLinks(); - } - - componentDidUpdate () { - this._updateStatusLinks(); - } - - onLinkClick = (e) => { - if (this.props.expanded === false) { - if (this.props.parseClick) this.props.parseClick(e); - } - } - - onMentionClick = (mention, e) => { - if (this.props.parseClick) { - this.props.parseClick(e, `/accounts/${mention.get('id')}`); - } - } - - onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, '').toLowerCase(); - - if (this.props.parseClick) { - this.props.parseClick(e, `/timelines/tag/${hashtag}`); - } - } - - handleMouseDown = (e) => { - this.startXY = [e.clientX, e.clientY]; - } - - handleMouseUp = (e) => { - const { parseClick } = this.props; - - if (!this.startXY) { - return; - } - - const [ startX, startY ] = this.startXY; - const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; - - if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) { - return; - } - - if (deltaX + deltaY < 5 && e.button === 0 && parseClick) { - parseClick(e); - } - - this.startXY = null; - } - - handleSpoilerClick = (e) => { - e.preventDefault(); - - if (this.props.setExpansion) { - this.props.setExpansion(this.props.expanded ? null : true); - } else { - this.setState({ hidden: !this.state.hidden }); - } - } - - setRef = (c) => { - this.node = c; - } - - render () { - const { - status, - media, - mediaIcon, - parseClick, - disabled, - } = this.props; - - const hidden = this.props.setExpansion ? !this.props.expanded : this.state.hidden; - - const content = { __html: status.get('contentHtml') }; - const spoilerContent = { __html: status.get('spoilerHtml') }; - const directionStyle = { direction: 'ltr' }; - const classNames = classnames('status__content', { - 'status__content--with-action': parseClick && !disabled, - 'status__content--with-spoiler': status.get('spoiler_text').length > 0, - }); - - if (isRtl(status.get('search_index'))) { - directionStyle.direction = 'rtl'; - } - - if (status.get('spoiler_text').length > 0) { - let mentionsPlaceholder = ''; - - const mentionLinks = status.get('mentions').map(item => ( - <Permalink - to={`/accounts/${item.get('id')}`} - href={item.get('url')} - key={item.get('id')} - className='mention' - > - @<span>{item.get('username')}</span> - </Permalink> - )).reduce((aggregate, item) => [...aggregate, item, ' '], []); - - const toggleText = hidden ? [ - <FormattedMessage - id='status.show_more' - defaultMessage='Show more' - key='0' - />, - mediaIcon ? ( - <i - className={ - `fa fa-fw fa-${mediaIcon} status__content__spoiler-icon` - } - aria-hidden='true' - key='1' - /> - ) : null, - ] : [ - <FormattedMessage - id='status.show_less' - defaultMessage='Show less' - key='0' - />, - ]; - - if (hidden) { - mentionsPlaceholder = <div>{mentionLinks}</div>; - } - - return ( - <div className={classNames} tabIndex='0'> - <p - style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} - > - <span dangerouslySetInnerHTML={spoilerContent} /> - {' '} - <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}> - {toggleText} - </button> - </p> - - {mentionsPlaceholder} - - <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}> - <div - ref={this.setRef} - style={directionStyle} - tabIndex={!hidden ? 0 : null} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} - dangerouslySetInnerHTML={content} - /> - {media} - </div> - - </div> - ); - } else if (parseClick) { - return ( - <div - className={classNames} - style={directionStyle} - tabIndex='0' - > - <div - ref={this.setRef} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} - dangerouslySetInnerHTML={content} - tabIndex='0' - /> - {media} - </div> - ); - } else { - return ( - <div - className='status__content' - style={directionStyle} - tabIndex='0' - > - <div ref={this.setRef} dangerouslySetInnerHTML={content} tabIndex='0' /> - {media} - </div> - ); - } - } - -} diff --git a/app/javascript/themes/glitch/components/status_header.js b/app/javascript/themes/glitch/components/status_header.js deleted file mode 100644 index bfa996cd5..000000000 --- a/app/javascript/themes/glitch/components/status_header.js +++ /dev/null @@ -1,120 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl } from 'react-intl'; - -// Mastodon imports. -import Avatar from './avatar'; -import AvatarOverlay from './avatar_overlay'; -import DisplayName from './display_name'; -import IconButton from './icon_button'; -import VisibilityIcon from './status_visibility_icon'; - -// Messages for use with internationalization stuff. -const messages = defineMessages({ - collapse: { id: 'status.collapse', defaultMessage: 'Collapse' }, - uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' }, - public: { id: 'privacy.public.short', defaultMessage: 'Public' }, - unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, -}); - -@injectIntl -export default class StatusHeader extends React.PureComponent { - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - friend: ImmutablePropTypes.map, - mediaIcon: PropTypes.string, - collapsible: PropTypes.bool, - collapsed: PropTypes.bool, - parseClick: PropTypes.func.isRequired, - setExpansion: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - // Handles clicks on collapsed button - handleCollapsedClick = (e) => { - const { collapsed, setExpansion } = this.props; - if (e.button === 0) { - setExpansion(collapsed ? null : false); - e.preventDefault(); - } - } - - // Handles clicks on account name/image - handleAccountClick = (e) => { - const { status, parseClick } = this.props; - parseClick(e, `/accounts/${+status.getIn(['account', 'id'])}`); - } - - // Rendering. - render () { - const { - status, - friend, - mediaIcon, - collapsible, - collapsed, - intl, - } = this.props; - - const account = status.get('account'); - - return ( - <header className='status__info'> - <a - href={account.get('url')} - target='_blank' - className='status__avatar' - onClick={this.handleAccountClick} - > - { - friend ? ( - <AvatarOverlay account={account} friend={friend} /> - ) : ( - <Avatar account={account} size={48} /> - ) - } - </a> - <a - href={account.get('url')} - target='_blank' - className='status__display-name' - onClick={this.handleAccountClick} - > - <DisplayName account={account} /> - </a> - <div className='status__info__icons'> - {mediaIcon ? ( - <i - className={`fa fa-fw fa-${mediaIcon}`} - aria-hidden='true' - /> - ) : null} - {( - <VisibilityIcon visibility={status.get('visibility')} /> - )} - {collapsible ? ( - <IconButton - className='status__collapse-button' - animate flip - active={collapsed} - title={ - collapsed ? - intl.formatMessage(messages.uncollapse) : - intl.formatMessage(messages.collapse) - } - icon='angle-double-up' - onClick={this.handleCollapsedClick} - /> - ) : null} - </div> - - </header> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_list.js b/app/javascript/themes/glitch/components/status_list.js deleted file mode 100644 index ddb1354c6..000000000 --- a/app/javascript/themes/glitch/components/status_list.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import StatusContainer from 'themes/glitch/containers/status_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ScrollableList from './scrollable_list'; - -export default class StatusList extends ImmutablePureComponent { - - static propTypes = { - scrollKey: PropTypes.string.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - onScrollToBottom: PropTypes.func, - onScrollToTop: PropTypes.func, - onScroll: PropTypes.func, - trackScroll: PropTypes.bool, - shouldUpdateScroll: PropTypes.func, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - prepend: PropTypes.node, - emptyMessage: PropTypes.node, - }; - - static defaultProps = { - trackScroll: true, - }; - - handleMoveUp = id => { - const elementIndex = this.props.statusIds.indexOf(id) - 1; - this._selectChild(elementIndex); - } - - handleMoveDown = id => { - const elementIndex = this.props.statusIds.indexOf(id) + 1; - this._selectChild(elementIndex); - } - - _selectChild (index) { - const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - element.focus(); - } - } - - setRef = c => { - this.node = c; - } - - render () { - const { statusIds, ...other } = this.props; - const { isLoading } = other; - - const scrollableContent = (isLoading || statusIds.size > 0) ? ( - statusIds.map((statusId) => ( - <StatusContainer - key={statusId} - id={statusId} - onMoveUp={this.handleMoveUp} - onMoveDown={this.handleMoveDown} - /> - )) - ) : null; - - return ( - <ScrollableList {...other} ref={this.setRef}> - {scrollableContent} - </ScrollableList> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_prepend.js b/app/javascript/themes/glitch/components/status_prepend.js deleted file mode 100644 index bd2559e46..000000000 --- a/app/javascript/themes/glitch/components/status_prepend.js +++ /dev/null @@ -1,83 +0,0 @@ -// Package imports // -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; - -export default class StatusPrepend extends React.PureComponent { - - static propTypes = { - type: PropTypes.string.isRequired, - account: ImmutablePropTypes.map.isRequired, - parseClick: PropTypes.func.isRequired, - notificationId: PropTypes.number, - }; - - handleClick = (e) => { - const { account, parseClick } = this.props; - parseClick(e, `/accounts/${+account.get('id')}`); - } - - Message = () => { - const { type, account } = this.props; - let link = ( - <a - onClick={this.handleClick} - href={account.get('url')} - className='status__display-name' - > - <b - dangerouslySetInnerHTML={{ - __html : account.get('display_name_html') || account.get('username'), - }} - /> - </a> - ); - switch (type) { - case 'reblogged_by': - return ( - <FormattedMessage - id='status.reblogged_by' - defaultMessage='{name} boosted' - values={{ name : link }} - /> - ); - case 'favourite': - return ( - <FormattedMessage - id='notification.favourite' - defaultMessage='{name} favourited your status' - values={{ name : link }} - /> - ); - case 'reblog': - return ( - <FormattedMessage - id='notification.reblog' - defaultMessage='{name} boosted your status' - values={{ name : link }} - /> - ); - } - return null; - } - - render () { - const { Message } = this; - const { type } = this.props; - - return !type ? null : ( - <aside className={type === 'reblogged_by' ? 'status__prepend' : 'notification__message'}> - <div className={type === 'reblogged_by' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}> - <i - className={`fa fa-fw fa-${ - type === 'favourite' ? 'star star-icon' : 'retweet' - } status__prepend-icon`} - /> - </div> - <Message /> - </aside> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_visibility_icon.js b/app/javascript/themes/glitch/components/status_visibility_icon.js deleted file mode 100644 index 017b69cbb..000000000 --- a/app/javascript/themes/glitch/components/status_visibility_icon.js +++ /dev/null @@ -1,48 +0,0 @@ -// Package imports // -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - public: { id: 'privacy.public.short', defaultMessage: 'Public' }, - unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, -}); - -@injectIntl -export default class VisibilityIcon extends ImmutablePureComponent { - - static propTypes = { - visibility: PropTypes.string, - intl: PropTypes.object.isRequired, - withLabel: PropTypes.bool, - }; - - render() { - const { withLabel, visibility, intl } = this.props; - - const visibilityClass = { - public: 'globe', - unlisted: 'unlock-alt', - private: 'lock', - direct: 'envelope', - }[visibility]; - - const label = intl.formatMessage(messages[visibility]); - - const icon = (<i - className={`status__visibility-icon fa fa-fw fa-${visibilityClass}`} - title={label} - aria-hidden='true' - />); - - if (withLabel) { - return (<span style={{ whiteSpace: 'nowrap' }}>{icon} {label}</span>); - } else { - return icon; - } - } - -} diff --git a/app/javascript/themes/glitch/containers/account_container.js b/app/javascript/themes/glitch/containers/account_container.js deleted file mode 100644 index c1ce49987..000000000 --- a/app/javascript/themes/glitch/containers/account_container.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { makeGetAccount } from 'themes/glitch/selectors'; -import Account from 'themes/glitch/components/account'; -import { - followAccount, - unfollowAccount, - blockAccount, - unblockAccount, - muteAccount, - unmuteAccount, -} from 'themes/glitch/actions/accounts'; -import { openModal } from 'themes/glitch/actions/modal'; -import { initMuteModal } from 'themes/glitch/actions/mutes'; -import { unfollowModal } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, props) => ({ - account: getAccount(state, props.id), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onFollow (account) { - if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { - if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - })); - } else { - dispatch(unfollowAccount(account.get('id'))); - } - } else { - dispatch(followAccount(account.get('id'))); - } - }, - - onBlock (account) { - if (account.getIn(['relationship', 'blocking'])) { - dispatch(unblockAccount(account.get('id'))); - } else { - dispatch(blockAccount(account.get('id'))); - } - }, - - onMute (account) { - if (account.getIn(['relationship', 'muting'])) { - dispatch(unmuteAccount(account.get('id'))); - } else { - dispatch(initMuteModal(account)); - } - }, - - - onMuteNotifications (account, notifications) { - dispatch(muteAccount(account.get('id'), notifications)); - }, -}); - -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account)); diff --git a/app/javascript/themes/glitch/containers/card_container.js b/app/javascript/themes/glitch/containers/card_container.js deleted file mode 100644 index 8285437bb..000000000 --- a/app/javascript/themes/glitch/containers/card_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Card from 'themes/glitch/features/status/components/card'; -import { fromJS } from 'immutable'; - -export default class CardContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string, - card: PropTypes.array.isRequired, - }; - - render () { - const { card, ...props } = this.props; - return <Card card={fromJS(card)} {...props} />; - } - -} diff --git a/app/javascript/themes/glitch/containers/compose_container.js b/app/javascript/themes/glitch/containers/compose_container.js deleted file mode 100644 index 82980ee36..000000000 --- a/app/javascript/themes/glitch/containers/compose_container.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import PropTypes from 'prop-types'; -import configureStore from 'themes/glitch/store/configureStore'; -import { hydrateStore } from 'themes/glitch/actions/store'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; -import Compose from 'themes/glitch/features/standalone/compose'; -import initialState from 'themes/glitch/util/initial_state'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -const store = configureStore(); - -if (initialState) { - store.dispatch(hydrateStore(initialState)); -} - -export default class TimelineContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - }; - - render () { - const { locale } = this.props; - - return ( - <IntlProvider locale={locale} messages={messages}> - <Provider store={store}> - <Compose /> - </Provider> - </IntlProvider> - ); - } - -} diff --git a/app/javascript/themes/glitch/containers/dropdown_menu_container.js b/app/javascript/themes/glitch/containers/dropdown_menu_container.js deleted file mode 100644 index 15e8da2e3..000000000 --- a/app/javascript/themes/glitch/containers/dropdown_menu_container.js +++ /dev/null @@ -1,16 +0,0 @@ -import { openModal, closeModal } from 'themes/glitch/actions/modal'; -import { connect } from 'react-redux'; -import DropdownMenu from 'themes/glitch/components/dropdown_menu'; -import { isUserTouching } from 'themes/glitch/util/is_mobile'; - -const mapStateToProps = state => ({ - isModalOpen: state.get('modal').modalType === 'ACTIONS', -}); - -const mapDispatchToProps = dispatch => ({ - isUserTouching, - onModalOpen: props => dispatch(openModal('ACTIONS', props)), - onModalClose: () => dispatch(closeModal()), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu); diff --git a/app/javascript/themes/glitch/containers/intersection_observer_article_container.js b/app/javascript/themes/glitch/containers/intersection_observer_article_container.js deleted file mode 100644 index 6ede64738..000000000 --- a/app/javascript/themes/glitch/containers/intersection_observer_article_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import IntersectionObserverArticle from 'themes/glitch/components/intersection_observer_article'; -import { setHeight } from 'themes/glitch/actions/height_cache'; - -const makeMapStateToProps = (state, props) => ({ - cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]), -}); - -const mapDispatchToProps = (dispatch) => ({ - - onHeightChange (key, id, height) { - dispatch(setHeight(key, id, height)); - }, - -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle); diff --git a/app/javascript/themes/glitch/containers/mastodon.js b/app/javascript/themes/glitch/containers/mastodon.js deleted file mode 100644 index 348470637..000000000 --- a/app/javascript/themes/glitch/containers/mastodon.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import PropTypes from 'prop-types'; -import configureStore from 'themes/glitch/store/configureStore'; -import { showOnboardingOnce } from 'themes/glitch/actions/onboarding'; -import { BrowserRouter, Route } from 'react-router-dom'; -import { ScrollContext } from 'react-router-scroll-4'; -import UI from 'themes/glitch/features/ui'; -import { hydrateStore } from 'themes/glitch/actions/store'; -import { connectUserStream } from 'themes/glitch/actions/streaming'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; -import initialState from 'themes/glitch/util/initial_state'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -export const store = configureStore(); -const hydrateAction = hydrateStore(initialState); -store.dispatch(hydrateAction); - -export default class Mastodon extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - }; - - componentDidMount() { - this.disconnect = store.dispatch(connectUserStream()); - - // Desktop notifications - // Ask after 1 minute - if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { - window.setTimeout(() => Notification.requestPermission(), 60 * 1000); - } - - // Protocol handler - // Ask after 5 minutes - if (typeof navigator.registerProtocolHandler !== 'undefined') { - const handlerUrl = window.location.protocol + '//' + window.location.host + '/intent?uri=%s'; - window.setTimeout(() => navigator.registerProtocolHandler('web+mastodon', handlerUrl, 'Mastodon'), 5 * 60 * 1000); - } - - store.dispatch(showOnboardingOnce()); - } - - componentWillUnmount () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - render () { - const { locale } = this.props; - - return ( - <IntlProvider locale={locale} messages={messages}> - <Provider store={store}> - <BrowserRouter basename='/web'> - <ScrollContext> - <Route path='/' component={UI} /> - </ScrollContext> - </BrowserRouter> - </Provider> - </IntlProvider> - ); - } - -} diff --git a/app/javascript/themes/glitch/containers/media_gallery_container.js b/app/javascript/themes/glitch/containers/media_gallery_container.js deleted file mode 100644 index 86965f73b..000000000 --- a/app/javascript/themes/glitch/containers/media_gallery_container.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; -import MediaGallery from 'themes/glitch/components/media_gallery'; -import { fromJS } from 'immutable'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -export default class MediaGalleryContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - media: PropTypes.array.isRequired, - }; - - handleOpenMedia = () => {} - - render () { - const { locale, media, ...props } = this.props; - - return ( - <IntlProvider locale={locale} messages={messages}> - <MediaGallery - {...props} - media={fromJS(media)} - onOpenMedia={this.handleOpenMedia} - /> - </IntlProvider> - ); - } - -} diff --git a/app/javascript/themes/glitch/containers/notification_purge_buttons_container.js b/app/javascript/themes/glitch/containers/notification_purge_buttons_container.js deleted file mode 100644 index ee4cb84cd..000000000 --- a/app/javascript/themes/glitch/containers/notification_purge_buttons_container.js +++ /dev/null @@ -1,49 +0,0 @@ -// Package imports. -import { connect } from 'react-redux'; -import { defineMessages, injectIntl } from 'react-intl'; - -// Our imports. -import NotificationPurgeButtons from 'themes/glitch/components/notification_purge_buttons'; -import { - deleteMarkedNotifications, - enterNotificationClearingMode, - markAllNotifications, -} from 'themes/glitch/actions/notifications'; -import { openModal } from 'themes/glitch/actions/modal'; - -const messages = defineMessages({ - clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' }, - clearConfirm: { id: 'notifications.marked_clear', defaultMessage: 'Clear selected notifications' }, -}); - -const mapDispatchToProps = (dispatch, { intl }) => ({ - onEnterCleaningMode(yes) { - dispatch(enterNotificationClearingMode(yes)); - }, - - onDeleteMarked() { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(deleteMarkedNotifications()), - })); - }, - - onMarkAll() { - dispatch(markAllNotifications(true)); - }, - - onMarkNone() { - dispatch(markAllNotifications(false)); - }, - - onInvert() { - dispatch(markAllNotifications(null)); - }, -}); - -const mapStateToProps = state => ({ - markNewForDelete: state.getIn(['notifications', 'markNewForDelete']), -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons)); diff --git a/app/javascript/themes/glitch/containers/status_container.js b/app/javascript/themes/glitch/containers/status_container.js deleted file mode 100644 index 0cee73b9a..000000000 --- a/app/javascript/themes/glitch/containers/status_container.js +++ /dev/null @@ -1,151 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import Status from 'themes/glitch/components/status'; -import { makeGetStatus } from 'themes/glitch/selectors'; -import { - replyCompose, - mentionCompose, -} from 'themes/glitch/actions/compose'; -import { - reblog, - favourite, - unreblog, - unfavourite, - pin, - unpin, -} from 'themes/glitch/actions/interactions'; -import { blockAccount } from 'themes/glitch/actions/accounts'; -import { muteStatus, unmuteStatus, deleteStatus } from 'themes/glitch/actions/statuses'; -import { initMuteModal } from 'themes/glitch/actions/mutes'; -import { initReport } from 'themes/glitch/actions/reports'; -import { openModal } from 'themes/glitch/actions/modal'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { boostModal, deleteModal } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, - deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, -}); - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = (state, props) => { - - let status = getStatus(state, props.id); - let reblogStatus = status ? status.get('reblog', null) : null; - let account = undefined; - let prepend = undefined; - - if (reblogStatus !== null && typeof reblogStatus === 'object') { - account = status.get('account'); - status = reblogStatus; - prepend = 'reblogged_by'; - } - - return { - containerId : props.containerId || props.id, // Should match reblogStatus's id for reblogs - status : status, - account : account || props.account, - settings : state.get('local_settings'), - prepend : prepend || props.prepend, - }; - }; - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onReply (status, router) { - dispatch(replyCompose(status, router)); - }, - - onModalReblog (status) { - dispatch(reblog(status)); - }, - - onReblog (status, e) { - if (status.get('reblogged')) { - dispatch(unreblog(status)); - } else { - if (e.shiftKey || !boostModal) { - this.onModalReblog(status); - } else { - dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); - } - } - }, - - onFavourite (status) { - if (status.get('favourited')) { - dispatch(unfavourite(status)); - } else { - dispatch(favourite(status)); - } - }, - - onPin (status) { - if (status.get('pinned')) { - dispatch(unpin(status)); - } else { - dispatch(pin(status)); - } - }, - - onEmbed (status) { - dispatch(openModal('EMBED', { url: status.get('url') })); - }, - - onDelete (status) { - if (!deleteModal) { - dispatch(deleteStatus(status.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'))), - })); - } - }, - - onMention (account, router) { - dispatch(mentionCompose(account, router)); - }, - - onOpenMedia (media, index) { - dispatch(openModal('MEDIA', { media, index })); - }, - - onOpenVideo (media, time) { - dispatch(openModal('VIDEO', { media, time })); - }, - - onBlock (account) { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - })); - }, - - onReport (status) { - dispatch(initReport(status.get('account'), status)); - }, - - onMute (account) { - dispatch(initMuteModal(account)); - }, - - onMuteConversation (status) { - if (status.get('muted')) { - dispatch(unmuteStatus(status.get('id'))); - } else { - dispatch(muteStatus(status.get('id'))); - } - }, - -}); - -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/themes/glitch/containers/timeline_container.js b/app/javascript/themes/glitch/containers/timeline_container.js deleted file mode 100644 index a75f8808d..000000000 --- a/app/javascript/themes/glitch/containers/timeline_container.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import PropTypes from 'prop-types'; -import configureStore from 'themes/glitch/store/configureStore'; -import { hydrateStore } from 'themes/glitch/actions/store'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; -import PublicTimeline from 'themes/glitch/features/standalone/public_timeline'; -import HashtagTimeline from 'themes/glitch/features/standalone/hashtag_timeline'; -import initialState from 'themes/glitch/util/initial_state'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -const store = configureStore(); - -if (initialState) { - store.dispatch(hydrateStore(initialState)); -} - -export default class TimelineContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - hashtag: PropTypes.string, - }; - - render () { - const { locale, hashtag } = this.props; - - let timeline; - - if (hashtag) { - timeline = <HashtagTimeline hashtag={hashtag} />; - } else { - timeline = <PublicTimeline />; - } - - return ( - <IntlProvider locale={locale} messages={messages}> - <Provider store={store}> - {timeline} - </Provider> - </IntlProvider> - ); - } - -} diff --git a/app/javascript/themes/glitch/containers/video_container.js b/app/javascript/themes/glitch/containers/video_container.js deleted file mode 100644 index 2b0e98666..000000000 --- a/app/javascript/themes/glitch/containers/video_container.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; -import Video from 'themes/glitch/features/video'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -export default class VideoContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - }; - - render () { - const { locale, ...props } = this.props; - - return ( - <IntlProvider locale={locale} messages={messages}> - <Video {...props} /> - </IntlProvider> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account/components/action_bar.js b/app/javascript/themes/glitch/features/account/components/action_bar.js deleted file mode 100644 index 0edd5c848..000000000 --- a/app/javascript/themes/glitch/features/account/components/action_bar.js +++ /dev/null @@ -1,145 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container'; -import { Link } from 'react-router-dom'; -import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; -import { me } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - report: { id: 'account.report', defaultMessage: 'Report @{name}' }, - share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' }, - media: { id: 'account.media', defaultMessage: 'Media' }, - blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, - hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, - showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, -}); - -@injectIntl -export default class ActionBar extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onFollow: PropTypes.func, - onBlock: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReblogToggle: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onBlockDomain: PropTypes.func.isRequired, - onUnblockDomain: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleShare = () => { - navigator.share({ - url: this.props.account.get('url'), - }); - } - - render () { - const { account, intl } = this.props; - - let menu = []; - let extraInfo = ''; - - menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); - if ('share' in navigator) { - menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); - } - menu.push(null); - menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` }); - menu.push(null); - - if (account.get('id') === me) { - menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); - } else { - const following = account.getIn(['relationship', 'following']); - if (following) { - if (following.get('reblogs')) { - menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); - } else { - menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); - } - } - - if (account.getIn(['relationship', 'muting'])) { - menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); - } else { - menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); - } - - if (account.getIn(['relationship', 'blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); - } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); - } - - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); - } - - if (account.get('acct') !== account.get('username')) { - const domain = account.get('acct').split('@')[1]; - - extraInfo = ( - <div className='account__disclaimer'> - <FormattedMessage - id='account.disclaimer_full' - defaultMessage="Information below may reflect the user's profile incompletely." - /> - {' '} - <a target='_blank' rel='noopener' href={account.get('url')}> - <FormattedMessage id='account.view_full_profile' defaultMessage='View full profile' /> - </a> - </div> - ); - - menu.push(null); - - if (account.getIn(['relationship', 'domain_blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain }); - } else { - menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain }); - } - } - - return ( - <div> - {extraInfo} - - <div className='account__action-bar'> - <div className='account__action-bar-dropdown'> - <DropdownMenuContainer items={menu} icon='bars' size={24} direction='right' /> - </div> - - <div className='account__action-bar-links'> - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> - <span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span> - <strong><FormattedNumber value={account.get('statuses_count')} /></strong> - </Link> - - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> - <span><FormattedMessage id='account.follows' defaultMessage='Follows' /></span> - <strong><FormattedNumber value={account.get('following_count')} /></strong> - </Link> - - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> - <span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span> - <strong><FormattedNumber value={account.get('followers_count')} /></strong> - </Link> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account/components/header.js b/app/javascript/themes/glitch/features/account/components/header.js deleted file mode 100644 index 696bb1991..000000000 --- a/app/javascript/themes/glitch/features/account/components/header.js +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import Avatar from 'themes/glitch/components/avatar'; -import IconButton from 'themes/glitch/components/icon_button'; - -import emojify from 'themes/glitch/util/emoji'; -import { me } from 'themes/glitch/util/initial_state'; -import { processBio } from 'themes/glitch/util/bio_metadata'; - -const messages = defineMessages({ - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, -}); - -@injectIntl -export default class Header extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - onFollow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { account, intl } = this.props; - - if (!account) { - return null; - } - - let displayName = account.get('display_name_html'); - let info = ''; - let actionBtn = ''; - - if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { - info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>; - } - - if (me !== account.get('id')) { - if (account.getIn(['relationship', 'requested'])) { - actionBtn = ( - <div className='account--action-button'> - <IconButton size={26} active icon='hourglass' title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} /> - </div> - ); - } else if (!account.getIn(['relationship', 'blocking'])) { - actionBtn = ( - <div className='account--action-button'> - <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> - </div> - ); - } - } - - const { text, metadata } = processBio(account.get('note')); - - return ( - <div className='account__header__wrapper'> - <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> - <div> - <Avatar account={account} size={90} /> - - <span className='account__header__display-name' dangerouslySetInnerHTML={{ __html: displayName }} /> - <span className='account__header__username'>@{account.get('acct')} {account.get('locked') ? <i className='fa fa-lock' /> : null}</span> - <div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} /> - - {info} - {actionBtn} - </div> - </div> - - {metadata.length && ( - <table className='account__metadata'> - <tbody> - {(() => { - let data = []; - for (let i = 0; i < metadata.length; i++) { - data.push( - <tr key={i}> - <th scope='row'><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][0]) }} /></th> - <td><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][1]) }} /></td> - </tr> - ); - } - return data; - })()} - </tbody> - </table> - ) || null} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account_gallery/components/media_item.js b/app/javascript/themes/glitch/features/account_gallery/components/media_item.js deleted file mode 100644 index 88c9156b5..000000000 --- a/app/javascript/themes/glitch/features/account_gallery/components/media_item.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Permalink from 'themes/glitch/components/permalink'; - -export default class MediaItem extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - }; - - render () { - const { media } = this.props; - const status = media.get('status'); - - let content, style; - - if (media.get('type') === 'gifv') { - content = <span className='media-gallery__gifv__label'>GIF</span>; - } - - if (!status.get('sensitive')) { - style = { backgroundImage: `url(${media.get('preview_url')})` }; - } - - return ( - <div className='account-gallery__item'> - <Permalink - to={`/statuses/${status.get('id')}`} - href={status.get('url')} - style={style} - > - {content} - </Permalink> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account_gallery/index.js b/app/javascript/themes/glitch/features/account_gallery/index.js deleted file mode 100644 index a21c089da..000000000 --- a/app/javascript/themes/glitch/features/account_gallery/index.js +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { fetchAccount } from 'themes/glitch/actions/accounts'; -import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from 'themes/glitch/actions/timelines'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { getAccountGallery } from 'themes/glitch/selectors'; -import MediaItem from './components/media_item'; -import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container'; -import { FormattedMessage } from 'react-intl'; -import { ScrollContainer } from 'react-router-scroll-4'; -import LoadMore from 'themes/glitch/components/load_more'; - -const mapStateToProps = (state, props) => ({ - medias: getAccountGallery(state, props.params.accountId), - isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), - hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']), -}); - -@connect(mapStateToProps) -export default class AccountGallery extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - medias: ImmutablePropTypes.list.isRequired, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - }; - - componentDidMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId)); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId)); - } - } - - handleScrollToBottom = () => { - if (this.props.hasMore) { - this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); - } - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - const offset = scrollHeight - scrollTop - clientHeight; - - if (150 > offset && !this.props.isLoading) { - this.handleScrollToBottom(); - } - } - - handleLoadMore = (e) => { - e.preventDefault(); - this.handleScrollToBottom(); - } - - render () { - const { medias, isLoading, hasMore } = this.props; - - let loadMore = null; - - if (!medias && isLoading) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - if (!isLoading && medias.size > 0 && hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='account_gallery'> - <div className='scrollable' onScroll={this.handleScroll}> - <HeaderContainer accountId={this.props.params.accountId} /> - - <div className='account-section-headline'> - <FormattedMessage id='account.media' defaultMessage='Media' /> - </div> - - <div className='account-gallery__container'> - {medias.map(media => - <MediaItem - key={media.get('id')} - media={media} - /> - )} - {loadMore} - </div> - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account_timeline/components/header.js b/app/javascript/themes/glitch/features/account_timeline/components/header.js deleted file mode 100644 index c719a7bcb..000000000 --- a/app/javascript/themes/glitch/features/account_timeline/components/header.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import InnerHeader from 'themes/glitch/features/account/components/header'; -import ActionBar from 'themes/glitch/features/account/components/action_bar'; -import MissingIndicator from 'themes/glitch/components/missing_indicator'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class Header extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReblogToggle: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onBlockDomain: PropTypes.func.isRequired, - onUnblockDomain: PropTypes.func.isRequired, - }; - - static contextTypes = { - router: PropTypes.object, - }; - - handleFollow = () => { - this.props.onFollow(this.props.account); - } - - handleBlock = () => { - this.props.onBlock(this.props.account); - } - - handleMention = () => { - this.props.onMention(this.props.account, this.context.router.history); - } - - handleReport = () => { - this.props.onReport(this.props.account); - } - - handleReblogToggle = () => { - this.props.onReblogToggle(this.props.account); - } - - handleMute = () => { - this.props.onMute(this.props.account); - } - - handleBlockDomain = () => { - const domain = this.props.account.get('acct').split('@')[1]; - - if (!domain) return; - - this.props.onBlockDomain(domain, this.props.account.get('id')); - } - - handleUnblockDomain = () => { - const domain = this.props.account.get('acct').split('@')[1]; - - if (!domain) return; - - this.props.onUnblockDomain(domain, this.props.account.get('id')); - } - - render () { - const { account } = this.props; - - if (account === null) { - return <MissingIndicator />; - } - - return ( - <div className='account-timeline__header'> - <InnerHeader - account={account} - onFollow={this.handleFollow} - /> - - <ActionBar - account={account} - onBlock={this.handleBlock} - onMention={this.handleMention} - onReblogToggle={this.handleReblogToggle} - onReport={this.handleReport} - onMute={this.handleMute} - onBlockDomain={this.handleBlockDomain} - onUnblockDomain={this.handleUnblockDomain} - /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js b/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js deleted file mode 100644 index 766b57b56..000000000 --- a/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { makeGetAccount } from 'themes/glitch/selectors'; -import Header from '../components/header'; -import { - followAccount, - unfollowAccount, - blockAccount, - unblockAccount, - unmuteAccount, -} from 'themes/glitch/actions/accounts'; -import { mentionCompose } from 'themes/glitch/actions/compose'; -import { initMuteModal } from 'themes/glitch/actions/mutes'; -import { initReport } from 'themes/glitch/actions/reports'; -import { openModal } from 'themes/glitch/actions/modal'; -import { blockDomain, unblockDomain } from 'themes/glitch/actions/domain_blocks'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { unfollowModal } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, - blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { accountId }) => ({ - account: getAccount(state, accountId), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onFollow (account) { - if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { - if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - })); - } else { - dispatch(unfollowAccount(account.get('id'))); - } - } else { - dispatch(followAccount(account.get('id'))); - } - }, - - onBlock (account) { - if (account.getIn(['relationship', 'blocking'])) { - dispatch(unblockAccount(account.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - })); - } - }, - - onMention (account, router) { - dispatch(mentionCompose(account, router)); - }, - - onReblogToggle (account) { - if (account.getIn(['relationship', 'following', 'reblogs'])) { - dispatch(followAccount(account.get('id'), false)); - } else { - dispatch(followAccount(account.get('id'), true)); - } - }, - - onReport (account) { - dispatch(initReport(account)); - }, - - onMute (account) { - if (account.getIn(['relationship', 'muting'])) { - dispatch(unmuteAccount(account.get('id'))); - } else { - dispatch(initMuteModal(account)); - } - }, - - onBlockDomain (domain, accountId) { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain, accountId)), - })); - }, - - onUnblockDomain (domain, accountId) { - dispatch(unblockDomain(domain, accountId)); - }, - -}); - -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/javascript/themes/glitch/features/account_timeline/index.js b/app/javascript/themes/glitch/features/account_timeline/index.js deleted file mode 100644 index 81336ef3a..000000000 --- a/app/javascript/themes/glitch/features/account_timeline/index.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { fetchAccount } from 'themes/glitch/actions/accounts'; -import { refreshAccountTimeline, expandAccountTimeline } from 'themes/glitch/actions/timelines'; -import StatusList from '../../components/status_list'; -import LoadingIndicator from '../../components/loading_indicator'; -import Column from '../ui/components/column'; -import HeaderContainer from './containers/header_container'; -import ColumnBackButton from '../../components/column_back_button'; -import { List as ImmutableList } from 'immutable'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()), - isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']), - hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']), -}); - -@connect(mapStateToProps) -export default class AccountTimeline extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - }; - - componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(refreshAccountTimeline(this.props.params.accountId)); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId)); - } - } - - handleScrollToBottom = () => { - if (!this.props.isLoading && this.props.hasMore) { - this.props.dispatch(expandAccountTimeline(this.props.params.accountId)); - } - } - - render () { - const { statusIds, isLoading, hasMore } = this.props; - - if (!statusIds && isLoading) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column name='account'> - <ColumnBackButton /> - - <StatusList - prepend={<HeaderContainer accountId={this.props.params.accountId} />} - scrollKey='account_timeline' - statusIds={statusIds} - isLoading={isLoading} - hasMore={hasMore} - onScrollToBottom={this.handleScrollToBottom} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/blocks/index.js b/app/javascript/themes/glitch/features/blocks/index.js deleted file mode 100644 index 70630818c..000000000 --- a/app/javascript/themes/glitch/features/blocks/index.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll-4'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import { fetchBlocks, expandBlocks } from 'themes/glitch/actions/blocks'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.blocks', defaultMessage: 'Blocked users' }, -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'blocks', 'items']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class Blocks extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - }; - - componentWillMount () { - this.props.dispatch(fetchBlocks()); - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandBlocks()); - } - } - - render () { - const { intl, accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column name='blocks' icon='ban' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='blocks'> - <div className='scrollable' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js b/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js deleted file mode 100644 index 988e36308..000000000 --- a/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import SettingText from 'themes/glitch/components/setting_text'; - -const messages = defineMessages({ - filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, - settings: { id: 'home.settings', defaultMessage: 'Column settings' }, -}); - -@injectIntl -export default class ColumnSettings extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { settings, onChange, intl } = this.props; - - return ( - <div> - <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> - - <div className='column-settings__row'> - <SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js b/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js deleted file mode 100644 index cd9c34365..000000000 --- a/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnSettings from '../components/column_settings'; -import { changeSetting } from 'themes/glitch/actions/settings'; - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'community']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (key, checked) { - dispatch(changeSetting(['community', ...key], checked)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/themes/glitch/features/community_timeline/index.js b/app/javascript/themes/glitch/features/community_timeline/index.js deleted file mode 100644 index 9d255bd01..000000000 --- a/app/javascript/themes/glitch/features/community_timeline/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - refreshCommunityTimeline, - expandCommunityTimeline, -} from 'themes/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectCommunityStream } from 'themes/glitch/actions/streaming'; - -const messages = defineMessages({ - title: { id: 'column.community', defaultMessage: 'Local timeline' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0, -}); - -@connect(mapStateToProps) -@injectIntl -export default class CommunityTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columnId: PropTypes.string, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('COMMUNITY', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - componentDidMount () { - const { dispatch } = this.props; - - dispatch(refreshCommunityTimeline()); - this.disconnect = dispatch(connectCommunityStream()); - } - - componentWillUnmount () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandCommunityTimeline()); - } - - render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='local'> - <ColumnHeader - icon='users' - active={hasUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - <StatusListContainer - trackScroll={!pinned} - scrollKey={`community_timeline-${columnId}`} - timelineId='community' - loadMore={this.handleLoadMore} - emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/advanced_options.js b/app/javascript/themes/glitch/features/compose/components/advanced_options.js deleted file mode 100644 index 045bad2e5..000000000 --- a/app/javascript/themes/glitch/features/compose/components/advanced_options.js +++ /dev/null @@ -1,62 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages } from 'react-intl'; - -// Our imports. -import ComposeAdvancedOptionsToggle from './advanced_options_toggle'; -import ComposeDropdown from './dropdown'; - -const messages = defineMessages({ - local_only_short : - { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, - local_only_long : - { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, - advanced_options_icon_title : - { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, -}); - -@injectIntl -export default class ComposeAdvancedOptions extends React.PureComponent { - - static propTypes = { - values : ImmutablePropTypes.contains({ - do_not_federate : PropTypes.bool.isRequired, - }).isRequired, - onChange : PropTypes.func.isRequired, - intl : PropTypes.object.isRequired, - }; - - render () { - const { intl, values } = this.props; - const options = [ - { icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, name: 'do_not_federate' }, - ]; - const anyEnabled = values.some((enabled) => enabled); - - const optionElems = options.map((option) => { - return ( - <ComposeAdvancedOptionsToggle - onChange={this.props.onChange} - active={values.get(option.name)} - key={option.name} - name={option.name} - shortText={intl.formatMessage(option.shortText)} - longText={intl.formatMessage(option.longText)} - /> - ); - }); - - return ( - <ComposeDropdown - title={intl.formatMessage(messages.advanced_options_icon_title)} - icon='home' - highlight={anyEnabled} - > - {optionElems} - </ComposeDropdown> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js b/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js deleted file mode 100644 index 98b3b6a44..000000000 --- a/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js +++ /dev/null @@ -1,35 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import Toggle from 'react-toggle'; - -export default class ComposeAdvancedOptionsToggle extends React.PureComponent { - - static propTypes = { - onChange: PropTypes.func.isRequired, - active: PropTypes.bool.isRequired, - name: PropTypes.string.isRequired, - shortText: PropTypes.string.isRequired, - longText: PropTypes.string.isRequired, - } - - onToggle = () => { - this.props.onChange(this.props.name); - } - - render() { - const { active, shortText, longText } = this.props; - return ( - <div role='button' tabIndex='0' className='advanced-options-dropdown__option' onClick={this.onToggle}> - <div className='advanced-options-dropdown__option__toggle'> - <Toggle checked={active} onChange={this.onToggle} /> - </div> - <div className='advanced-options-dropdown__option__content'> - <strong>{shortText}</strong> - {longText} - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/attach_options.js b/app/javascript/themes/glitch/features/compose/components/attach_options.js deleted file mode 100644 index c396714f3..000000000 --- a/app/javascript/themes/glitch/features/compose/components/attach_options.js +++ /dev/null @@ -1,131 +0,0 @@ -// Package imports // -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { injectIntl, defineMessages } from 'react-intl'; - -// Our imports // -import ComposeDropdown from './dropdown'; -import { uploadCompose } from 'themes/glitch/actions/compose'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { openModal } from 'themes/glitch/actions/modal'; - -const messages = defineMessages({ - upload : - { id: 'compose.attach.upload', defaultMessage: 'Upload a file' }, - doodle : - { id: 'compose.attach.doodle', defaultMessage: 'Draw something' }, - attach : - { id: 'compose.attach', defaultMessage: 'Attach...' }, -}); - -const mapStateToProps = state => ({ - // This horrible expression is copied from vanilla upload_button_container - disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), - resetFileKey: state.getIn(['compose', 'resetFileKey']), - acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), -}); - -const mapDispatchToProps = dispatch => ({ - onSelectFile (files) { - dispatch(uploadCompose(files)); - }, - onOpenDoodle () { - dispatch(openModal('DOODLE', { noEsc: true })); - }, -}); - -@injectIntl -@connect(mapStateToProps, mapDispatchToProps) -export default class ComposeAttachOptions extends ImmutablePureComponent { - - static propTypes = { - intl : PropTypes.object.isRequired, - resetFileKey: PropTypes.number, - acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired, - disabled: PropTypes.bool, - onSelectFile: PropTypes.func.isRequired, - onOpenDoodle: PropTypes.func.isRequired, - }; - - handleItemClick = bt => { - if (bt === 'upload') { - this.fileElement.click(); - } - - if (bt === 'doodle') { - this.props.onOpenDoodle(); - } - - this.dropdown.setState({ open: false }); - }; - - handleFileChange = (e) => { - if (e.target.files.length > 0) { - this.props.onSelectFile(e.target.files); - } - } - - setFileRef = (c) => { - this.fileElement = c; - } - - setDropdownRef = (c) => { - this.dropdown = c; - } - - render () { - const { intl, resetFileKey, disabled, acceptContentTypes } = this.props; - - const options = [ - { icon: 'cloud-upload', text: messages.upload, name: 'upload' }, - { icon: 'paint-brush', text: messages.doodle, name: 'doodle' }, - ]; - - const optionElems = options.map((item) => { - const hdl = () => this.handleItemClick(item.name); - return ( - <div - role='button' - tabIndex='0' - key={item.name} - onClick={hdl} - className='privacy-dropdown__option' - > - <div className='privacy-dropdown__option__icon'> - <i className={`fa fa-fw fa-${item.icon}`} /> - </div> - - <div className='privacy-dropdown__option__content'> - <strong>{intl.formatMessage(item.text)}</strong> - </div> - </div> - ); - }); - - return ( - <div> - <ComposeDropdown - title={intl.formatMessage(messages.attach)} - icon='paperclip' - disabled={disabled} - ref={this.setDropdownRef} - > - {optionElems} - </ComposeDropdown> - <input - key={resetFileKey} - ref={this.setFileRef} - type='file' - multiple={false} - accept={acceptContentTypes.toArray().join(',')} - onChange={this.handleFileChange} - disabled={disabled} - style={{ display: 'none' }} - /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js b/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js deleted file mode 100644 index 4a98d89fe..000000000 --- a/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import Avatar from 'themes/glitch/components/avatar'; -import DisplayName from 'themes/glitch/components/display_name'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class AutosuggestAccount extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - }; - - render () { - const { account } = this.props; - - return ( - <div className='autosuggest-account'> - <div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div> - <DisplayName account={account} /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/character_counter.js b/app/javascript/themes/glitch/features/compose/components/character_counter.js deleted file mode 100644 index 0ecfc9141..000000000 --- a/app/javascript/themes/glitch/features/compose/components/character_counter.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { length } from 'stringz'; - -export default class CharacterCounter extends React.PureComponent { - - static propTypes = { - text: PropTypes.string.isRequired, - max: PropTypes.number.isRequired, - }; - - checkRemainingText (diff) { - if (diff < 0) { - return <span className='character-counter character-counter--over'>{diff}</span>; - } - - return <span className='character-counter'>{diff}</span>; - } - - render () { - const diff = this.props.max - length(this.props.text); - return this.checkRemainingText(diff); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/compose_form.js b/app/javascript/themes/glitch/features/compose/components/compose_form.js deleted file mode 100644 index 54b1944a4..000000000 --- a/app/javascript/themes/glitch/features/compose/components/compose_form.js +++ /dev/null @@ -1,286 +0,0 @@ -import React from 'react'; -import CharacterCounter from './character_counter'; -import Button from 'themes/glitch/components/button'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ReplyIndicatorContainer from '../containers/reply_indicator_container'; -import AutosuggestTextarea from 'themes/glitch/components/autosuggest_textarea'; -import { defineMessages, injectIntl } from 'react-intl'; -import Collapsable from 'themes/glitch/components/collapsable'; -import SpoilerButtonContainer from '../containers/spoiler_button_container'; -import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; -import ComposeAdvancedOptionsContainer from '../containers/advanced_options_container'; -import SensitiveButtonContainer from '../containers/sensitive_button_container'; -import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; -import UploadFormContainer from '../containers/upload_form_container'; -import WarningContainer from '../containers/warning_container'; -import { isMobile } from 'themes/glitch/util/is_mobile'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { length } from 'stringz'; -import { countableText } from 'themes/glitch/util/counter'; -import ComposeAttachOptions from './attach_options'; -import initialState from 'themes/glitch/util/initial_state'; - -const maxChars = initialState.max_toot_chars; - -const messages = defineMessages({ - placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, - spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, - publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }, - publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, -}); - -@injectIntl -export default class ComposeForm extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - text: PropTypes.string.isRequired, - suggestion_token: PropTypes.string, - suggestions: ImmutablePropTypes.list, - spoiler: PropTypes.bool, - privacy: PropTypes.string, - advanced_options: ImmutablePropTypes.contains({ - do_not_federate: PropTypes.bool, - }), - spoiler_text: PropTypes.string, - focusDate: PropTypes.instanceOf(Date), - preselectDate: PropTypes.instanceOf(Date), - is_submitting: PropTypes.bool, - is_uploading: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onClearSuggestions: PropTypes.func.isRequired, - onFetchSuggestions: PropTypes.func.isRequired, - onPrivacyChange: PropTypes.func.isRequired, - onSuggestionSelected: PropTypes.func.isRequired, - onChangeSpoilerText: PropTypes.func.isRequired, - onPaste: PropTypes.func.isRequired, - onPickEmoji: PropTypes.func.isRequired, - showSearch: PropTypes.bool, - settings : ImmutablePropTypes.map.isRequired, - }; - - static defaultProps = { - showSearch: false, - }; - - handleChange = (e) => { - this.props.onChange(e.target.value); - } - - handleKeyDown = (e) => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); - } - } - - handleSubmit2 = () => { - this.props.onPrivacyChange(this.props.settings.get('side_arm')); - this.handleSubmit(); - } - - handleSubmit = () => { - if (this.props.text !== this.autosuggestTextarea.textarea.value) { - // Something changed the text inside the textarea (e.g. browser extensions like Grammarly) - // Update the state to match the current text - this.props.onChange(this.autosuggestTextarea.textarea.value); - } - - this.props.onSubmit(); - } - - onSuggestionsClearRequested = () => { - this.props.onClearSuggestions(); - } - - onSuggestionsFetchRequested = (token) => { - this.props.onFetchSuggestions(token); - } - - onSuggestionSelected = (tokenStart, token, value) => { - this._restoreCaret = null; - this.props.onSuggestionSelected(tokenStart, token, value); - } - - handleChangeSpoilerText = (e) => { - this.props.onChangeSpoilerText(e.target.value); - } - - componentWillReceiveProps (nextProps) { - // If this is the update where we've finished uploading, - // save the last caret position so we can restore it below! - if (!nextProps.is_uploading && this.props.is_uploading) { - this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart; - } - } - - componentDidUpdate (prevProps) { - // This statement does several things: - // - If we're beginning a reply, and, - // - Replying to zero or one users, places the cursor at the end of the textbox. - // - Replying to more than one user, selects any usernames past the first; - // this provides a convenient shortcut to drop everyone else from the conversation. - // - If we've just finished uploading an image, and have a saved caret position, - // restores the cursor to that position after the text changes! - if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) { - let selectionEnd, selectionStart; - - if (this.props.preselectDate !== prevProps.preselectDate) { - selectionEnd = this.props.text.length; - selectionStart = this.props.text.search(/\s/) + 1; - } else if (typeof this._restoreCaret === 'number') { - selectionStart = this._restoreCaret; - selectionEnd = this._restoreCaret; - } else { - selectionEnd = this.props.text.length; - selectionStart = selectionEnd; - } - - this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd); - this.autosuggestTextarea.textarea.focus(); - } else if(prevProps.is_submitting && !this.props.is_submitting) { - this.autosuggestTextarea.textarea.focus(); - } - } - - setAutosuggestTextarea = (c) => { - this.autosuggestTextarea = c; - } - - handleEmojiPick = (data) => { - const position = this.autosuggestTextarea.textarea.selectionStart; - const emojiChar = data.native; - this._restoreCaret = position + emojiChar.length + 1; - this.props.onPickEmoji(position, data); - } - - render () { - const { intl, onPaste, showSearch } = this.props; - const disabled = this.props.is_submitting; - const maybeEye = (this.props.advanced_options && this.props.advanced_options.do_not_federate) ? ' 👁️' : ''; - const text = [this.props.spoiler_text, countableText(this.props.text), maybeEye].join(''); - - const secondaryVisibility = this.props.settings.get('side_arm'); - let showSideArm = secondaryVisibility !== 'none'; - - let publishText = ''; - let publishText2 = ''; - let title = ''; - let title2 = ''; - - const privacyIcons = { - none: '', - public: 'globe', - unlisted: 'unlock-alt', - private: 'lock', - direct: 'envelope', - }; - - title = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${this.props.privacy}.short` })}`; - - if (showSideArm) { - // Enhanced behavior with dual toot buttons - publishText = ( - <span> - { - <i - className={`fa fa-${privacyIcons[this.props.privacy]}`} - style={{ paddingRight: '5px' }} - /> - }{intl.formatMessage(messages.publish)} - </span> - ); - - title2 = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${secondaryVisibility}.short` })}`; - publishText2 = ( - <i - className={`fa fa-${privacyIcons[secondaryVisibility]}`} - aria-label={title2} - /> - ); - } else { - // Original vanilla behavior - no icon if public or unlisted - if (this.props.privacy === 'private' || this.props.privacy === 'direct') { - publishText = <span className='compose-form__publish-private'><i className='fa fa-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); - } - } - - const submitDisabled = disabled || this.props.is_uploading || length(text) > maxChars || (text.length !== 0 && text.trim().length === 0); - - return ( - <div className='compose-form'> - <Collapsable isVisible={this.props.spoiler} fullHeight={50}> - <div className='spoiler-input'> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span> - <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' /> - </label> - </div> - </Collapsable> - - <WarningContainer /> - - <ReplyIndicatorContainer /> - - <div className='compose-form__autosuggest-wrapper'> - <AutosuggestTextarea - ref={this.setAutosuggestTextarea} - placeholder={intl.formatMessage(messages.placeholder)} - disabled={disabled} - value={this.props.text} - onChange={this.handleChange} - suggestions={this.props.suggestions} - onKeyDown={this.handleKeyDown} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - onSuggestionSelected={this.onSuggestionSelected} - onPaste={onPaste} - autoFocus={!showSearch && !isMobile(window.innerWidth)} - /> - - <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> - </div> - - <div className='compose-form__modifiers'> - <UploadFormContainer /> - </div> - - <div className='compose-form__buttons'> - <ComposeAttachOptions /> - <SensitiveButtonContainer /> - <div className='compose-form__buttons-separator' /> - <PrivacyDropdownContainer /> - <SpoilerButtonContainer /> - <ComposeAdvancedOptionsContainer /> - </div> - - <div className='compose-form__publish'> - <div className='character-counter__wrapper'><CharacterCounter max={maxChars} text={text} /></div> - <div className='compose-form__publish-button-wrapper'> - { - showSideArm ? - <Button - className='compose-form__publish__side-arm' - text={publishText2} - title={title2} - onClick={this.handleSubmit2} - disabled={submitDisabled} - /> : '' - } - <Button - className='compose-form__publish__primary' - text={publishText} - title={title} - onClick={this.handleSubmit} - disabled={submitDisabled} - /> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/dropdown.js b/app/javascript/themes/glitch/features/compose/components/dropdown.js deleted file mode 100644 index f3d9f094e..000000000 --- a/app/javascript/themes/glitch/features/compose/components/dropdown.js +++ /dev/null @@ -1,77 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; - -// Our imports. -import IconButton from 'themes/glitch/components/icon_button'; - -const iconStyle = { - height : null, - lineHeight : '27px', -}; - -export default class ComposeDropdown extends React.PureComponent { - - static propTypes = { - title: PropTypes.string.isRequired, - icon: PropTypes.string, - highlight: PropTypes.bool, - disabled: PropTypes.bool, - children: PropTypes.arrayOf(PropTypes.node).isRequired, - }; - - state = { - open: false, - }; - - onGlobalClick = (e) => { - if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { - this.setState({ open: false }); - } - }; - - componentDidMount () { - window.addEventListener('click', this.onGlobalClick); - window.addEventListener('touchstart', this.onGlobalClick); - } - componentWillUnmount () { - window.removeEventListener('click', this.onGlobalClick); - window.removeEventListener('touchstart', this.onGlobalClick); - } - - onToggleDropdown = () => { - if (this.props.disabled) return; - this.setState({ open: !this.state.open }); - }; - - setRef = (c) => { - this.node = c; - }; - - render () { - const { open } = this.state; - let { highlight, title, icon, disabled } = this.props; - - if (!icon) icon = 'ellipsis-h'; - - return ( - <div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${highlight ? 'active' : ''} `}> - <div className='advanced-options-dropdown__value'> - <IconButton - className={'inverted'} - title={title} - icon={icon} active={open || highlight} - size={18} - style={iconStyle} - disabled={disabled} - onClick={this.onToggleDropdown} - /> - </div> - <div className='advanced-options-dropdown__dropdown'> - {this.props.children} - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js b/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js deleted file mode 100644 index fd59efb85..000000000 --- a/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js +++ /dev/null @@ -1,376 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import { EmojiPicker as EmojiPickerAsync } from 'themes/glitch/util/async-components'; -import Overlay from 'react-overlays/lib/Overlay'; -import classNames from 'classnames'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import detectPassiveEvents from 'detect-passive-events'; -import { buildCustomEmojis } from 'themes/glitch/util/emoji'; - -const messages = defineMessages({ - emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, - emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, - emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' }, - custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' }, - recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' }, - search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' }, - people: { id: 'emoji_button.people', defaultMessage: 'People' }, - nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' }, - food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' }, - activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' }, - travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' }, - objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' }, - symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' }, - flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }, -}); - -const assetHost = process.env.CDN_HOST || ''; -let EmojiPicker, Emoji; // load asynchronously - -const backgroundImageFn = () => `${assetHost}/emoji/sheet.png`; -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; - -const categoriesSort = [ - 'recent', - 'custom', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', -]; - -class ModifierPickerMenu extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - onSelect: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - }; - - handleClick = e => { - this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.active) { - this.attachListeners(); - } else { - this.removeListeners(); - } - } - - componentWillUnmount () { - this.removeListeners(); - } - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - attachListeners () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - removeListeners () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - render () { - const { active } = this.props; - - return ( - <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}> - <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button> - </div> - ); - } - -} - -class ModifierPicker extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - modifier: PropTypes.number, - onChange: PropTypes.func, - onClose: PropTypes.func, - onOpen: PropTypes.func, - }; - - handleClick = () => { - if (this.props.active) { - this.props.onClose(); - } else { - this.props.onOpen(); - } - } - - handleSelect = modifier => { - this.props.onChange(modifier); - this.props.onClose(); - } - - render () { - const { active, modifier } = this.props; - - return ( - <div className='emoji-picker-dropdown__modifiers'> - <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> - <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} /> - </div> - ); - } - -} - -@injectIntl -class EmojiPickerMenu extends React.PureComponent { - - static propTypes = { - custom_emojis: ImmutablePropTypes.list, - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - loading: PropTypes.bool, - onClose: PropTypes.func.isRequired, - onPick: PropTypes.func.isRequired, - style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, - intl: PropTypes.object.isRequired, - skinTone: PropTypes.number.isRequired, - onSkinTone: PropTypes.func.isRequired, - }; - - static defaultProps = { - style: {}, - loading: true, - placement: 'bottom', - frequentlyUsedEmojis: [], - }; - - state = { - modifierOpen: false, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - getI18n = () => { - const { intl } = this.props; - - return { - search: intl.formatMessage(messages.emoji_search), - notfound: intl.formatMessage(messages.emoji_not_found), - categories: { - search: intl.formatMessage(messages.search_results), - recent: intl.formatMessage(messages.recent), - people: intl.formatMessage(messages.people), - nature: intl.formatMessage(messages.nature), - foods: intl.formatMessage(messages.food), - activity: intl.formatMessage(messages.activity), - places: intl.formatMessage(messages.travel), - objects: intl.formatMessage(messages.objects), - symbols: intl.formatMessage(messages.symbols), - flags: intl.formatMessage(messages.flags), - custom: intl.formatMessage(messages.custom), - }, - }; - } - - handleClick = emoji => { - if (!emoji.native) { - emoji.native = emoji.colons; - } - - this.props.onClose(); - this.props.onPick(emoji); - } - - handleModifierOpen = () => { - this.setState({ modifierOpen: true }); - } - - handleModifierClose = () => { - this.setState({ modifierOpen: false }); - } - - handleModifierChange = modifier => { - this.props.onSkinTone(modifier); - } - - render () { - const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props; - - if (loading) { - return <div style={{ width: 299 }} />; - } - - const title = intl.formatMessage(messages.emoji); - const { modifierOpen } = this.state; - - return ( - <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> - <EmojiPicker - perLine={8} - emojiSize={22} - sheetSize={32} - custom={buildCustomEmojis(custom_emojis)} - color='' - emoji='' - set='twitter' - title={title} - i18n={this.getI18n()} - onClick={this.handleClick} - include={categoriesSort} - recent={frequentlyUsedEmojis} - skin={skinTone} - showPreview={false} - backgroundImageFn={backgroundImageFn} - emojiTooltip - /> - - <ModifierPicker - active={modifierOpen} - modifier={skinTone} - onOpen={this.handleModifierOpen} - onClose={this.handleModifierClose} - onChange={this.handleModifierChange} - /> - </div> - ); - } - -} - -@injectIntl -export default class EmojiPickerDropdown extends React.PureComponent { - - static propTypes = { - custom_emojis: ImmutablePropTypes.list, - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - intl: PropTypes.object.isRequired, - onPickEmoji: PropTypes.func.isRequired, - onSkinTone: PropTypes.func.isRequired, - skinTone: PropTypes.number.isRequired, - }; - - state = { - active: false, - loading: false, - }; - - setRef = (c) => { - this.dropdown = c; - } - - onShowDropdown = () => { - this.setState({ active: true }); - - if (!EmojiPicker) { - this.setState({ loading: true }); - - EmojiPickerAsync().then(EmojiMart => { - EmojiPicker = EmojiMart.Picker; - Emoji = EmojiMart.Emoji; - - this.setState({ loading: false }); - }).catch(() => { - this.setState({ loading: false }); - }); - } - } - - onHideDropdown = () => { - this.setState({ active: false }); - } - - onToggle = (e) => { - if (!this.state.loading && (!e.key || e.key === 'Enter')) { - if (this.state.active) { - this.onHideDropdown(); - } else { - this.onShowDropdown(); - } - } - } - - handleKeyDown = e => { - if (e.key === 'Escape') { - this.onHideDropdown(); - } - } - - setTargetRef = c => { - this.target = c; - } - - findTarget = () => { - return this.target; - } - - render () { - const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; - const title = intl.formatMessage(messages.emoji); - const { active, loading } = this.state; - - return ( - <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> - <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}> - <img - className={classNames('emojione', { 'pulse-loading': active && loading })} - alt='🙂' - src={`${assetHost}/emoji/1f602.svg`} - /> - </div> - - <Overlay show={active} placement='bottom' target={this.findTarget}> - <EmojiPickerMenu - custom_emojis={this.props.custom_emojis} - loading={loading} - onClose={this.onHideDropdown} - onPick={onPickEmoji} - onSkinTone={onSkinTone} - skinTone={skinTone} - frequentlyUsedEmojis={frequentlyUsedEmojis} - /> - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/navigation_bar.js b/app/javascript/themes/glitch/features/compose/components/navigation_bar.js deleted file mode 100644 index 24a70949b..000000000 --- a/app/javascript/themes/glitch/features/compose/components/navigation_bar.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from 'themes/glitch/components/avatar'; -import IconButton from 'themes/glitch/components/icon_button'; -import Permalink from 'themes/glitch/components/permalink'; -import { FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class NavigationBar extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onClose: PropTypes.func.isRequired, - }; - - render () { - return ( - <div className='navigation-bar'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> - <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> - <Avatar account={this.props.account} size={40} /> - </Permalink> - - <div className='navigation-bar__profile'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> - <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong> - </Permalink> - - <a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> - </div> - - <IconButton title='' icon='close' onClick={this.props.onClose} /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js deleted file mode 100644 index 0cd92d174..000000000 --- a/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js +++ /dev/null @@ -1,200 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, defineMessages } from 'react-intl'; -import IconButton from 'themes/glitch/components/icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import detectPassiveEvents from 'detect-passive-events'; -import classNames from 'classnames'; - -const messages = defineMessages({ - public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, - public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, - unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, - direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' }, - change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, -}); - -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; - -class PrivacyDropdownMenu extends React.PureComponent { - - static propTypes = { - style: PropTypes.object, - items: PropTypes.array.isRequired, - value: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - handleClick = e => { - if (e.key === 'Escape') { - this.props.onClose(); - } else if (!e.key || e.key === 'Enter') { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); - - this.props.onClose(); - this.props.onChange(value); - } - } - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - render () { - const { style, items, value } = this.props; - - return ( - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}> - {items.map(item => - <div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })}> - <div className='privacy-dropdown__option__icon'> - <i className={`fa fa-fw fa-${item.icon}`} /> - </div> - - <div className='privacy-dropdown__option__content'> - <strong>{item.text}</strong> - {item.meta} - </div> - </div> - )} - </div> - )} - </Motion> - ); - } - -} - -@injectIntl -export default class PrivacyDropdown extends React.PureComponent { - - static propTypes = { - isUserTouching: PropTypes.func, - isModalOpen: PropTypes.bool.isRequired, - onModalOpen: PropTypes.func, - onModalClose: PropTypes.func, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - open: false, - }; - - handleToggle = () => { - if (this.props.isUserTouching()) { - if (this.state.open) { - this.props.onModalClose(); - } else { - this.props.onModalOpen({ - actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })), - onClick: this.handleModalActionClick, - }); - } - } else { - this.setState({ open: !this.state.open }); - } - } - - handleModalActionClick = (e) => { - e.preventDefault(); - - const { value } = this.options[e.currentTarget.getAttribute('data-index')]; - - this.props.onModalClose(); - this.props.onChange(value); - } - - handleKeyDown = e => { - switch(e.key) { - case 'Enter': - this.handleToggle(); - break; - case 'Escape': - this.handleClose(); - break; - } - } - - handleClose = () => { - this.setState({ open: false }); - } - - handleChange = value => { - this.props.onChange(value); - } - - componentWillMount () { - const { intl: { formatMessage } } = this.props; - - this.options = [ - { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, - { icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, - { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, - { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, - ]; - } - - render () { - const { value, intl } = this.props; - const { open } = this.state; - - const valueOption = this.options.find(item => item.value === value); - - return ( - <div className={classNames('privacy-dropdown', { active: open })} onKeyDown={this.handleKeyDown}> - <div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}> - <IconButton - className='privacy-dropdown__value-icon' - icon={valueOption.icon} - title={intl.formatMessage(messages.change_privacy)} - size={18} - expanded={open} - active={open} - inverted - onClick={this.handleToggle} - style={{ height: null, lineHeight: '27px' }} - /> - </div> - - <Overlay show={open} placement='bottom' target={this}> - <PrivacyDropdownMenu - items={this.options} - value={value} - onClose={this.handleClose} - onChange={this.handleChange} - /> - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/reply_indicator.js b/app/javascript/themes/glitch/features/compose/components/reply_indicator.js deleted file mode 100644 index 9a8d10ceb..000000000 --- a/app/javascript/themes/glitch/features/compose/components/reply_indicator.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from 'themes/glitch/components/avatar'; -import IconButton from 'themes/glitch/components/icon_button'; -import DisplayName from 'themes/glitch/components/display_name'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, -}); - -@injectIntl -export default class ReplyIndicator extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map, - onCancel: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleClick = () => { - this.props.onCancel(); - } - - handleAccountClick = (e) => { - if (e.button === 0) { - e.preventDefault(); - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - } - - render () { - const { status, intl } = this.props; - - if (!status) { - return null; - } - - const content = { __html: status.get('contentHtml') }; - - return ( - <div className='reply-indicator'> - <div className='reply-indicator__header'> - <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div> - - <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'> - <div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div> - <DisplayName account={status.get('account')} /> - </a> - </div> - - <div className='reply-indicator__content' dangerouslySetInnerHTML={content} /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/search.js b/app/javascript/themes/glitch/features/compose/components/search.js deleted file mode 100644 index c3218137f..000000000 --- a/app/javascript/themes/glitch/features/compose/components/search.js +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; - -const messages = defineMessages({ - placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, -}); - -class SearchPopout extends React.PureComponent { - - static propTypes = { - style: PropTypes.object, - }; - - render () { - const { style } = this.props; - - return ( - <div style={{ ...style, position: 'absolute', width: 285 }}> - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> - <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> - - <ul> - <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li> - <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> - </ul> - - <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' /> - </div> - )} - </Motion> - </div> - ); - } - -} - -@injectIntl -export default class Search extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - submitted: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - onShow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - expanded: false, - }; - - handleChange = (e) => { - this.props.onChange(e.target.value); - } - - handleClear = (e) => { - e.preventDefault(); - - if (this.props.value.length > 0 || this.props.submitted) { - this.props.onClear(); - } - } - - handleKeyDown = (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.props.onSubmit(); - } else if (e.key === 'Escape') { - document.querySelector('.ui').parentElement.focus(); - } - } - - noop () { - - } - - handleFocus = () => { - this.setState({ expanded: true }); - this.props.onShow(); - } - - handleBlur = () => { - this.setState({ expanded: false }); - } - - render () { - const { intl, value, submitted } = this.props; - const { expanded } = this.state; - const hasValue = value.length > 0 || submitted; - - return ( - <div className='search'> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span> - <input - className='search__input' - type='text' - placeholder={intl.formatMessage(messages.placeholder)} - value={value} - onChange={this.handleChange} - onKeyUp={this.handleKeyDown} - onFocus={this.handleFocus} - onBlur={this.handleBlur} - /> - </label> - - <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> - <i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> - <i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} /> - </div> - - <Overlay show={expanded && !hasValue} placement='bottom' target={this}> - <SearchPopout /> - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/search_results.js b/app/javascript/themes/glitch/features/compose/components/search_results.js deleted file mode 100644 index 3fdafa5f3..000000000 --- a/app/javascript/themes/glitch/features/compose/components/search_results.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import StatusContainer from 'themes/glitch/containers/status_container'; -import { Link } from 'react-router-dom'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class SearchResults extends ImmutablePureComponent { - - static propTypes = { - results: ImmutablePropTypes.map.isRequired, - }; - - render () { - const { results } = this.props; - - let accounts, statuses, hashtags; - let count = 0; - - if (results.get('accounts') && results.get('accounts').size > 0) { - count += results.get('accounts').size; - accounts = ( - <div className='search-results__section'> - {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} - </div> - ); - } - - if (results.get('statuses') && results.get('statuses').size > 0) { - count += results.get('statuses').size; - statuses = ( - <div className='search-results__section'> - {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} - </div> - ); - } - - if (results.get('hashtags') && results.get('hashtags').size > 0) { - count += results.get('hashtags').size; - hashtags = ( - <div className='search-results__section'> - {results.get('hashtags').map(hashtag => - <Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}> - #{hashtag} - </Link> - )} - </div> - ); - } - - return ( - <div className='search-results'> - <div className='search-results__header'> - <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> - </div> - - {accounts} - {statuses} - {hashtags} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/text_icon_button.js b/app/javascript/themes/glitch/features/compose/components/text_icon_button.js deleted file mode 100644 index 9c8ffab1f..000000000 --- a/app/javascript/themes/glitch/features/compose/components/text_icon_button.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class TextIconButton extends React.PureComponent { - - static propTypes = { - label: PropTypes.string.isRequired, - title: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func.isRequired, - ariaControls: PropTypes.string, - }; - - handleClick = (e) => { - e.preventDefault(); - this.props.onClick(); - } - - render () { - const { label, title, active, ariaControls } = this.props; - - return ( - <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}> - {label} - </button> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/upload.js b/app/javascript/themes/glitch/features/compose/components/upload.js deleted file mode 100644 index ded376ada..000000000 --- a/app/javascript/themes/glitch/features/compose/components/upload.js +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from 'themes/glitch/components/icon_button'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; -import classNames from 'classnames'; - -const messages = defineMessages({ - undo: { id: 'upload_form.undo', defaultMessage: 'Undo' }, - description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, -}); - -@injectIntl -export default class Upload extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, - onUndo: PropTypes.func.isRequired, - onDescriptionChange: PropTypes.func.isRequired, - }; - - state = { - hovered: false, - focused: false, - dirtyDescription: null, - }; - - handleUndoClick = () => { - this.props.onUndo(this.props.media.get('id')); - } - - handleInputChange = e => { - this.setState({ dirtyDescription: e.target.value }); - } - - handleMouseEnter = () => { - this.setState({ hovered: true }); - } - - handleMouseLeave = () => { - this.setState({ hovered: false }); - } - - handleInputFocus = () => { - this.setState({ focused: true }); - } - - handleInputBlur = () => { - const { dirtyDescription } = this.state; - - this.setState({ focused: false, dirtyDescription: null }); - - if (dirtyDescription !== null) { - this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription); - } - } - - render () { - const { intl, media } = this.props; - const active = this.state.hovered || this.state.focused; - const description = this.state.dirtyDescription || media.get('description') || ''; - - return ( - <div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> - <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> - {({ scale }) => ( - <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}> - <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} /> - - <div className={classNames('compose-form__upload-description', { active })}> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span> - - <input - placeholder={intl.formatMessage(messages.description)} - type='text' - value={description} - maxLength={420} - onFocus={this.handleInputFocus} - onChange={this.handleInputChange} - onBlur={this.handleInputBlur} - /> - </label> - </div> - </div> - )} - </Motion> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/upload_button.js b/app/javascript/themes/glitch/features/compose/components/upload_button.js deleted file mode 100644 index d7742adfe..000000000 --- a/app/javascript/themes/glitch/features/compose/components/upload_button.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import IconButton from 'themes/glitch/components/icon_button'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -const messages = defineMessages({ - upload: { id: 'upload_button.label', defaultMessage: 'Add media' }, -}); - -const makeMapStateToProps = () => { - const mapStateToProps = state => ({ - acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), - }); - - return mapStateToProps; -}; - -const iconStyle = { - height: null, - lineHeight: '27px', -}; - -@connect(makeMapStateToProps) -@injectIntl -export default class UploadButton extends ImmutablePureComponent { - - static propTypes = { - disabled: PropTypes.bool, - onSelectFile: PropTypes.func.isRequired, - style: PropTypes.object, - resetFileKey: PropTypes.number, - acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired, - intl: PropTypes.object.isRequired, - }; - - handleChange = (e) => { - if (e.target.files.length > 0) { - this.props.onSelectFile(e.target.files); - } - } - - handleClick = () => { - this.fileElement.click(); - } - - setRef = (c) => { - this.fileElement = c; - } - - render () { - - const { intl, resetFileKey, disabled, acceptContentTypes } = this.props; - - return ( - <div className='compose-form__upload-button'> - <IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span> - <input - key={resetFileKey} - ref={this.setRef} - type='file' - multiple={false} - accept={acceptContentTypes.toArray().join(',')} - onChange={this.handleChange} - disabled={disabled} - style={{ display: 'none' }} - /> - </label> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/upload_form.js b/app/javascript/themes/glitch/features/compose/components/upload_form.js deleted file mode 100644 index b7f112205..000000000 --- a/app/javascript/themes/glitch/features/compose/components/upload_form.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import UploadProgressContainer from '../containers/upload_progress_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import UploadContainer from '../containers/upload_container'; - -export default class UploadForm extends ImmutablePureComponent { - - static propTypes = { - mediaIds: ImmutablePropTypes.list.isRequired, - }; - - render () { - const { mediaIds } = this.props; - - return ( - <div className='compose-form__upload-wrapper'> - <UploadProgressContainer /> - - <div className='compose-form__uploads-wrapper'> - {mediaIds.map(id => ( - <UploadContainer id={id} key={id} /> - ))} - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/upload_progress.js b/app/javascript/themes/glitch/features/compose/components/upload_progress.js deleted file mode 100644 index b923d0a22..000000000 --- a/app/javascript/themes/glitch/features/compose/components/upload_progress.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { FormattedMessage } from 'react-intl'; - -export default class UploadProgress extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - progress: PropTypes.number, - }; - - render () { - const { active, progress } = this.props; - - if (!active) { - return null; - } - - return ( - <div className='upload-progress'> - <div className='upload-progress__icon'> - <i className='fa fa-upload' /> - </div> - - <div className='upload-progress__message'> - <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' /> - - <div className='upload-progress__backdrop'> - <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> - {({ width }) => - <div className='upload-progress__tracker' style={{ width: `${width}%` }} /> - } - </Motion> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/warning.js b/app/javascript/themes/glitch/features/compose/components/warning.js deleted file mode 100644 index 82df55a31..000000000 --- a/app/javascript/themes/glitch/features/compose/components/warning.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; - -export default class Warning extends React.PureComponent { - - static propTypes = { - message: PropTypes.node.isRequired, - }; - - render () { - const { message } = this.props; - - return ( - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='compose-form__warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> - {message} - </div> - )} - </Motion> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js b/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js deleted file mode 100644 index 9f168942a..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js +++ /dev/null @@ -1,20 +0,0 @@ -// Package imports. -import { connect } from 'react-redux'; - -// Our imports. -import { toggleComposeAdvancedOption } from 'themes/glitch/actions/compose'; -import ComposeAdvancedOptions from '../components/advanced_options'; - -const mapStateToProps = state => ({ - values: state.getIn(['compose', 'advanced_options']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (option) { - dispatch(toggleComposeAdvancedOption(option)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ComposeAdvancedOptions); diff --git a/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js b/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js deleted file mode 100644 index 96eb70c18..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js +++ /dev/null @@ -1,15 +0,0 @@ -import { connect } from 'react-redux'; -import AutosuggestAccount from '../components/autosuggest_account'; -import { makeGetAccount } from 'themes/glitch/selectors'; - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { id }) => ({ - account: getAccount(state, id), - }); - - return mapStateToProps; -}; - -export default connect(makeMapStateToProps)(AutosuggestAccount); diff --git a/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js b/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js deleted file mode 100644 index 7afa988f1..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js +++ /dev/null @@ -1,71 +0,0 @@ -import { connect } from 'react-redux'; -import ComposeForm from '../components/compose_form'; -import { changeComposeVisibility, uploadCompose } from 'themes/glitch/actions/compose'; -import { - changeCompose, - submitCompose, - clearComposeSuggestions, - fetchComposeSuggestions, - selectComposeSuggestion, - changeComposeSpoilerText, - insertEmojiCompose, -} from 'themes/glitch/actions/compose'; - -const mapStateToProps = state => ({ - text: state.getIn(['compose', 'text']), - suggestion_token: state.getIn(['compose', 'suggestion_token']), - suggestions: state.getIn(['compose', 'suggestions']), - advanced_options: state.getIn(['compose', 'advanced_options']), - spoiler: state.getIn(['compose', 'spoiler']), - spoiler_text: state.getIn(['compose', 'spoiler_text']), - privacy: state.getIn(['compose', 'privacy']), - focusDate: state.getIn(['compose', 'focusDate']), - preselectDate: state.getIn(['compose', 'preselectDate']), - is_submitting: state.getIn(['compose', 'is_submitting']), - is_uploading: state.getIn(['compose', 'is_uploading']), - showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), - settings: state.get('local_settings'), - filesAttached: state.getIn(['compose', 'media_attachments']).size > 0, -}); - -const mapDispatchToProps = (dispatch) => ({ - - onChange (text) { - dispatch(changeCompose(text)); - }, - - onPrivacyChange (value) { - dispatch(changeComposeVisibility(value)); - }, - - onSubmit () { - dispatch(submitCompose()); - }, - - onClearSuggestions () { - dispatch(clearComposeSuggestions()); - }, - - onFetchSuggestions (token) { - dispatch(fetchComposeSuggestions(token)); - }, - - onSuggestionSelected (position, token, accountId) { - dispatch(selectComposeSuggestion(position, token, accountId)); - }, - - onChangeSpoilerText (checked) { - dispatch(changeComposeSpoilerText(checked)); - }, - - onPaste (files) { - dispatch(uploadCompose(files)); - }, - - onPickEmoji (position, data) { - dispatch(insertEmojiCompose(position, data)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm); diff --git a/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js deleted file mode 100644 index 55a13bd65..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js +++ /dev/null @@ -1,82 +0,0 @@ -import { connect } from 'react-redux'; -import EmojiPickerDropdown from '../components/emoji_picker_dropdown'; -import { changeSetting } from 'themes/glitch/actions/settings'; -import { createSelector } from 'reselect'; -import { Map as ImmutableMap } from 'immutable'; -import { useEmoji } from 'themes/glitch/actions/emojis'; - -const perLine = 8; -const lines = 2; - -const DEFAULTS = [ - '+1', - 'grinning', - 'kissing_heart', - 'heart_eyes', - 'laughing', - 'stuck_out_tongue_winking_eye', - 'sweat_smile', - 'joy', - 'yum', - 'disappointed', - 'thinking_face', - 'weary', - 'sob', - 'sunglasses', - 'heart', - 'ok_hand', -]; - -const getFrequentlyUsedEmojis = createSelector([ - state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()), -], emojiCounters => { - let emojis = emojiCounters - .keySeq() - .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b)) - .reverse() - .slice(0, perLine * lines) - .toArray(); - - if (emojis.length < DEFAULTS.length) { - emojis = emojis.concat(DEFAULTS.slice(0, DEFAULTS.length - emojis.length)); - } - - return emojis; -}); - -const getCustomEmojis = createSelector([ - state => state.get('custom_emojis'), -], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => { - const aShort = a.get('shortcode').toLowerCase(); - const bShort = b.get('shortcode').toLowerCase(); - - if (aShort < bShort) { - return -1; - } else if (aShort > bShort ) { - return 1; - } else { - return 0; - } -})); - -const mapStateToProps = state => ({ - custom_emojis: getCustomEmojis(state), - skinTone: state.getIn(['settings', 'skinTone']), - frequentlyUsedEmojis: getFrequentlyUsedEmojis(state), -}); - -const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({ - onSkinTone: skinTone => { - dispatch(changeSetting(['skinTone'], skinTone)); - }, - - onPickEmoji: emoji => { - dispatch(useEmoji(emoji)); - - if (onPickEmoji) { - onPickEmoji(emoji); - } - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown); diff --git a/app/javascript/themes/glitch/features/compose/containers/navigation_container.js b/app/javascript/themes/glitch/features/compose/containers/navigation_container.js deleted file mode 100644 index b6d737b46..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/navigation_container.js +++ /dev/null @@ -1,11 +0,0 @@ -import { connect } from 'react-redux'; -import NavigationBar from '../components/navigation_bar'; -import { me } from 'themes/glitch/util/initial_state'; - -const mapStateToProps = state => { - return { - account: state.getIn(['accounts', me]), - }; -}; - -export default connect(mapStateToProps)(NavigationBar); diff --git a/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js b/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js deleted file mode 100644 index 9636ceab2..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js +++ /dev/null @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import PrivacyDropdown from '../components/privacy_dropdown'; -import { changeComposeVisibility } from 'themes/glitch/actions/compose'; -import { openModal, closeModal } from 'themes/glitch/actions/modal'; -import { isUserTouching } from 'themes/glitch/util/is_mobile'; - -const mapStateToProps = state => ({ - isModalOpen: state.get('modal').modalType === 'ACTIONS', - value: state.getIn(['compose', 'privacy']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (value) { - dispatch(changeComposeVisibility(value)); - }, - - isUserTouching, - onModalOpen: props => dispatch(openModal('ACTIONS', props)), - onModalClose: () => dispatch(closeModal()), - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown); diff --git a/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js b/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js deleted file mode 100644 index 6dcabb3cd..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js +++ /dev/null @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import { cancelReplyCompose } from 'themes/glitch/actions/compose'; -import { makeGetStatus } from 'themes/glitch/selectors'; -import ReplyIndicator from '../components/reply_indicator'; - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = state => ({ - status: getStatus(state, state.getIn(['compose', 'in_reply_to'])), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = dispatch => ({ - - onCancel () { - dispatch(cancelReplyCompose()); - }, - -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator); diff --git a/app/javascript/themes/glitch/features/compose/containers/search_container.js b/app/javascript/themes/glitch/features/compose/containers/search_container.js deleted file mode 100644 index a450d27e7..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/search_container.js +++ /dev/null @@ -1,35 +0,0 @@ -import { connect } from 'react-redux'; -import { - changeSearch, - clearSearch, - submitSearch, - showSearch, -} from 'themes/glitch/actions/search'; -import Search from '../components/search'; - -const mapStateToProps = state => ({ - value: state.getIn(['search', 'value']), - submitted: state.getIn(['search', 'submitted']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (value) { - dispatch(changeSearch(value)); - }, - - onClear () { - dispatch(clearSearch()); - }, - - onSubmit () { - dispatch(submitSearch()); - }, - - onShow () { - dispatch(showSearch()); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Search); diff --git a/app/javascript/themes/glitch/features/compose/containers/search_results_container.js b/app/javascript/themes/glitch/features/compose/containers/search_results_container.js deleted file mode 100644 index 16d95d417..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/search_results_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import SearchResults from '../components/search_results'; - -const mapStateToProps = state => ({ - results: state.getIn(['search', 'results']), -}); - -export default connect(mapStateToProps)(SearchResults); diff --git a/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js b/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js deleted file mode 100644 index a710dd104..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import IconButton from 'themes/glitch/components/icon_button'; -import { changeComposeSensitivity } from 'themes/glitch/actions/compose'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { injectIntl, defineMessages } from 'react-intl'; - -const messages = defineMessages({ - title: { id: 'compose_form.sensitive', defaultMessage: 'Mark media as sensitive' }, -}); - -const mapStateToProps = state => ({ - visible: state.getIn(['compose', 'media_attachments']).size > 0, - active: state.getIn(['compose', 'sensitive']), - disabled: state.getIn(['compose', 'spoiler']), -}); - -const mapDispatchToProps = dispatch => ({ - - onClick () { - dispatch(changeComposeSensitivity()); - }, - -}); - -class SensitiveButton extends React.PureComponent { - - static propTypes = { - visible: PropTypes.bool, - active: PropTypes.bool, - disabled: PropTypes.bool, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { visible, active, disabled, onClick, intl } = this.props; - - return ( - <Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}> - {({ scale }) => { - const icon = active ? 'eye-slash' : 'eye'; - const className = classNames('compose-form__sensitive-button', { - 'compose-form__sensitive-button--visible': visible, - }); - return ( - <div className={className} style={{ transform: `scale(${scale})` }}> - <IconButton - className='compose-form__sensitive-button__icon' - title={intl.formatMessage(messages.title)} - icon={icon} - onClick={onClick} - size={18} - active={active} - disabled={disabled} - style={{ lineHeight: null, height: null }} - inverted - /> - </div> - ); - }} - </Motion> - ); - } - -} - -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); diff --git a/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js b/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js deleted file mode 100644 index 160e71ba9..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js +++ /dev/null @@ -1,25 +0,0 @@ -import { connect } from 'react-redux'; -import TextIconButton from '../components/text_icon_button'; -import { changeComposeSpoilerness } from 'themes/glitch/actions/compose'; -import { injectIntl, defineMessages } from 'react-intl'; - -const messages = defineMessages({ - title: { id: 'compose_form.spoiler', defaultMessage: 'Hide text behind warning' }, -}); - -const mapStateToProps = (state, { intl }) => ({ - label: 'CW', - title: intl.formatMessage(messages.title), - active: state.getIn(['compose', 'spoiler']), - ariaControls: 'cw-spoiler-input', -}); - -const mapDispatchToProps = dispatch => ({ - - onClick () { - dispatch(changeComposeSpoilerness()); - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton)); diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js b/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js deleted file mode 100644 index f332eae1a..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from 'react-redux'; -import UploadButton from '../components/upload_button'; -import { uploadCompose } from 'themes/glitch/actions/compose'; - -const mapStateToProps = state => ({ - disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), - resetFileKey: state.getIn(['compose', 'resetFileKey']), -}); - -const mapDispatchToProps = dispatch => ({ - - onSelectFile (files) { - dispatch(uploadCompose(files)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(UploadButton); diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_container.js b/app/javascript/themes/glitch/features/compose/containers/upload_container.js deleted file mode 100644 index eea514bf5..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/upload_container.js +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux'; -import Upload from '../components/upload'; -import { undoUploadCompose, changeUploadCompose } from 'themes/glitch/actions/compose'; - -const mapStateToProps = (state, { id }) => ({ - media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), -}); - -const mapDispatchToProps = dispatch => ({ - - onUndo: id => { - dispatch(undoUploadCompose(id)); - }, - - onDescriptionChange: (id, description) => { - dispatch(changeUploadCompose(id, description)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Upload); diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js b/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js deleted file mode 100644 index a6798bf51..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import UploadForm from '../components/upload_form'; - -const mapStateToProps = state => ({ - mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')), -}); - -export default connect(mapStateToProps)(UploadForm); diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js b/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js deleted file mode 100644 index 0cfee96da..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js +++ /dev/null @@ -1,9 +0,0 @@ -import { connect } from 'react-redux'; -import UploadProgress from '../components/upload_progress'; - -const mapStateToProps = state => ({ - active: state.getIn(['compose', 'is_uploading']), - progress: state.getIn(['compose', 'progress']), -}); - -export default connect(mapStateToProps)(UploadProgress); diff --git a/app/javascript/themes/glitch/features/compose/containers/warning_container.js b/app/javascript/themes/glitch/features/compose/containers/warning_container.js deleted file mode 100644 index 225d6a1dd..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/warning_container.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import Warning from '../components/warning'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { me } from 'themes/glitch/util/initial_state'; - -const mapStateToProps = state => ({ - needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), -}); - -const WarningWrapper = ({ needsLockWarning }) => { - if (needsLockWarning) { - return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />; - } - - return null; -}; - -WarningWrapper.propTypes = { - needsLockWarning: PropTypes.bool, -}; - -export default connect(mapStateToProps)(WarningWrapper); diff --git a/app/javascript/themes/glitch/features/compose/index.js b/app/javascript/themes/glitch/features/compose/index.js deleted file mode 100644 index 3fcaf416f..000000000 --- a/app/javascript/themes/glitch/features/compose/index.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import ComposeFormContainer from './containers/compose_form_container'; -import NavigationContainer from './containers/navigation_container'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; -import { mountCompose, unmountCompose } from 'themes/glitch/actions/compose'; -import { openModal } from 'themes/glitch/actions/modal'; -import { changeLocalSetting } from 'themes/glitch/actions/local_settings'; -import { Link } from 'react-router-dom'; -import { injectIntl, defineMessages } from 'react-intl'; -import SearchContainer from './containers/search_container'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import SearchResultsContainer from './containers/search_results_container'; -import { changeComposing } from 'themes/glitch/actions/compose'; - -const messages = defineMessages({ - start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, - home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, - notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, - public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, - community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, - settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, - logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, -}); - -const mapStateToProps = state => ({ - columns: state.getIn(['settings', 'columns']), - showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class Compose extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columns: ImmutablePropTypes.list.isRequired, - multiColumn: PropTypes.bool, - showSearch: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - componentDidMount () { - this.props.dispatch(mountCompose()); - } - - componentWillUnmount () { - this.props.dispatch(unmountCompose()); - } - - onLayoutClick = (e) => { - const layout = e.currentTarget.getAttribute('data-mastodon-layout'); - this.props.dispatch(changeLocalSetting(['layout'], layout)); - e.preventDefault(); - } - - openSettings = () => { - this.props.dispatch(openModal('SETTINGS', {})); - } - - onFocus = () => { - this.props.dispatch(changeComposing(true)); - } - - onBlur = () => { - this.props.dispatch(changeComposing(false)); - } - - render () { - const { multiColumn, showSearch, intl } = this.props; - - let header = ''; - - if (multiColumn) { - const { columns } = this.props; - header = ( - <nav className='drawer__header'> - <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-asterisk' /></Link> - {!columns.some(column => column.get('id') === 'HOME') && ( - <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' /></Link> - )} - {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( - <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' /></Link> - )} - {!columns.some(column => column.get('id') === 'COMMUNITY') && ( - <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><i role='img' className='fa fa-fw fa-users' /></Link> - )} - {!columns.some(column => column.get('id') === 'PUBLIC') && ( - <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link> - )} - <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)} aria-label={intl.formatMessage(messages.settings)}><i role='img' className='fa fa-fw fa-cogs' /></a> - <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a> - </nav> - ); - } - - - - return ( - <div className='drawer'> - {header} - - <SearchContainer /> - - <div className='drawer__pager'> - <div className='drawer__inner scrollable optionally-scrollable' onFocus={this.onFocus}> - <NavigationContainer onClose={this.onBlur} /> - <ComposeFormContainer /> - </div> - - <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> - {({ x }) => - <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> - <SearchResultsContainer /> - </div> - } - </Motion> - </div> - - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js b/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js deleted file mode 100644 index 2a40c65a5..000000000 --- a/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnSettings from 'themes/glitch/features/community_timeline/components/column_settings'; -import { changeSetting } from 'themes/glitch/actions/settings'; - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'direct']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (key, checked) { - dispatch(changeSetting(['direct', ...key], checked)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/themes/glitch/features/direct_timeline/index.js b/app/javascript/themes/glitch/features/direct_timeline/index.js deleted file mode 100644 index 6b29cf94d..000000000 --- a/app/javascript/themes/glitch/features/direct_timeline/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - refreshDirectTimeline, - expandDirectTimeline, -} from 'themes/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectDirectStream } from 'themes/glitch/actions/streaming'; - -const messages = defineMessages({ - title: { id: 'column.direct', defaultMessage: 'Direct messages' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0, -}); - -@connect(mapStateToProps) -@injectIntl -export default class DirectTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columnId: PropTypes.string, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('DIRECT', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - componentDidMount () { - const { dispatch } = this.props; - - dispatch(refreshDirectTimeline()); - this.disconnect = dispatch(connectDirectStream()); - } - - componentWillUnmount () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandDirectTimeline()); - } - - render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef}> - <ColumnHeader - icon='envelope' - active={hasUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - <StatusListContainer - trackScroll={!pinned} - scrollKey={`direct_timeline-${columnId}`} - timelineId='direct' - loadMore={this.handleLoadMore} - emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/favourited_statuses/index.js b/app/javascript/themes/glitch/features/favourited_statuses/index.js deleted file mode 100644 index 80345e0e2..000000000 --- a/app/javascript/themes/glitch/features/favourited_statuses/index.js +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'themes/glitch/actions/favourites'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import StatusList from 'themes/glitch/components/status_list'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.favourites', defaultMessage: 'Favourites' }, -}); - -const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'favourites', 'items']), - hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class Favourites extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - intl: PropTypes.object.isRequired, - columnId: PropTypes.string, - multiColumn: PropTypes.bool, - hasMore: PropTypes.bool, - }; - - componentWillMount () { - this.props.dispatch(fetchFavouritedStatuses()); - } - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('FAVOURITES', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - handleScrollToBottom = () => { - this.props.dispatch(expandFavouritedStatuses()); - } - - render () { - const { intl, statusIds, columnId, multiColumn, hasMore } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='favourites'> - <ColumnHeader - icon='star' - title={intl.formatMessage(messages.heading)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - showBackButton - /> - - <StatusList - trackScroll={!pinned} - statusIds={statusIds} - scrollKey={`favourited_statuses-${columnId}`} - hasMore={hasMore} - onScrollToBottom={this.handleScrollToBottom} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/favourites/index.js b/app/javascript/themes/glitch/features/favourites/index.js deleted file mode 100644 index d7b8ac3b1..000000000 --- a/app/javascript/themes/glitch/features/favourites/index.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { fetchFavourites } from 'themes/glitch/actions/interactions'; -import { ScrollContainer } from 'react-router-scroll-4'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), -}); - -@connect(mapStateToProps) -export default class Favourites extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - }; - - componentWillMount () { - this.props.dispatch(fetchFavourites(this.props.params.statusId)); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchFavourites(nextProps.params.statusId)); - } - } - - render () { - const { accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='favourites'> - <div className='scrollable'> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js b/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js deleted file mode 100644 index ce386d888..000000000 --- a/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Permalink from 'themes/glitch/components/permalink'; -import Avatar from 'themes/glitch/components/avatar'; -import DisplayName from 'themes/glitch/components/display_name'; -import IconButton from 'themes/glitch/components/icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, - reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, -}); - -@injectIntl -export default class AccountAuthorize extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onAuthorize: PropTypes.func.isRequired, - onReject: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { intl, account, onAuthorize, onReject } = this.props; - const content = { __html: account.get('note_emojified') }; - - return ( - <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> - <DisplayName account={account} /> - </Permalink> - - <div className='account__header__content' dangerouslySetInnerHTML={content} /> - </div> - - <div className='account--panel'> - <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div> - <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js b/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js deleted file mode 100644 index 78ae77eee..000000000 --- a/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js +++ /dev/null @@ -1,26 +0,0 @@ -import { connect } from 'react-redux'; -import { makeGetAccount } from 'themes/glitch/selectors'; -import AccountAuthorize from '../components/account_authorize'; -import { authorizeFollowRequest, rejectFollowRequest } from 'themes/glitch/actions/accounts'; - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, props) => ({ - account: getAccount(state, props.id), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { id }) => ({ - onAuthorize () { - dispatch(authorizeFollowRequest(id)); - }, - - onReject () { - dispatch(rejectFollowRequest(id)); - }, -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(AccountAuthorize); diff --git a/app/javascript/themes/glitch/features/follow_requests/index.js b/app/javascript/themes/glitch/features/follow_requests/index.js deleted file mode 100644 index 3f44f518a..000000000 --- a/app/javascript/themes/glitch/features/follow_requests/index.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll-4'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import AccountAuthorizeContainer from './containers/account_authorize_container'; -import { fetchFollowRequests, expandFollowRequests } from 'themes/glitch/actions/accounts'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class FollowRequests extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - }; - - componentWillMount () { - this.props.dispatch(fetchFollowRequests()); - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandFollowRequests()); - } - } - - render () { - const { intl, accountIds } = this.props; - - if (!accountIds) { - return ( - <Column name='follow-requests'> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column name='follow-requests' icon='users' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - - <ScrollContainer scrollKey='follow_requests'> - <div className='scrollable' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountAuthorizeContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/followers/index.js b/app/javascript/themes/glitch/features/followers/index.js deleted file mode 100644 index d586bf41d..000000000 --- a/app/javascript/themes/glitch/features/followers/index.js +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { - fetchAccount, - fetchFollowers, - expandFollowers, -} from 'themes/glitch/actions/accounts'; -import { ScrollContainer } from 'react-router-scroll-4'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import Column from 'themes/glitch/features/ui/components/column'; -import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container'; -import LoadMore from 'themes/glitch/components/load_more'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']), - hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), -}); - -@connect(mapStateToProps) -export default class Followers extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - hasMore: PropTypes.bool, - }; - - componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowers(this.props.params.accountId)); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(fetchFollowers(nextProps.params.accountId)); - } - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) { - this.props.dispatch(expandFollowers(this.props.params.accountId)); - } - } - - handleLoadMore = (e) => { - e.preventDefault(); - this.props.dispatch(expandFollowers(this.props.params.accountId)); - } - - render () { - const { accountIds, hasMore } = this.props; - - let loadMore = null; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - if (hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='followers'> - <div className='scrollable' onScroll={this.handleScroll}> - <div className='followers'> - <HeaderContainer accountId={this.props.params.accountId} /> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - {loadMore} - </div> - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/following/index.js b/app/javascript/themes/glitch/features/following/index.js deleted file mode 100644 index c306faf21..000000000 --- a/app/javascript/themes/glitch/features/following/index.js +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { - fetchAccount, - fetchFollowing, - expandFollowing, -} from 'themes/glitch/actions/accounts'; -import { ScrollContainer } from 'react-router-scroll-4'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import Column from 'themes/glitch/features/ui/components/column'; -import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container'; -import LoadMore from 'themes/glitch/components/load_more'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']), - hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), -}); - -@connect(mapStateToProps) -export default class Following extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - hasMore: PropTypes.bool, - }; - - componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowing(this.props.params.accountId)); - } - - 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)); - } - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) { - this.props.dispatch(expandFollowing(this.props.params.accountId)); - } - } - - handleLoadMore = (e) => { - e.preventDefault(); - this.props.dispatch(expandFollowing(this.props.params.accountId)); - } - - render () { - const { accountIds, hasMore } = this.props; - - let loadMore = null; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - if (hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='following'> - <div className='scrollable' onScroll={this.handleScroll}> - <div className='following'> - <HeaderContainer accountId={this.props.params.accountId} /> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - {loadMore} - </div> - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/generic_not_found/index.js b/app/javascript/themes/glitch/features/generic_not_found/index.js deleted file mode 100644 index ccd2b87b2..000000000 --- a/app/javascript/themes/glitch/features/generic_not_found/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import Column from 'themes/glitch/features/ui/components/column'; -import MissingIndicator from 'themes/glitch/components/missing_indicator'; - -const GenericNotFound = () => ( - <Column> - <MissingIndicator /> - </Column> -); - -export default GenericNotFound; diff --git a/app/javascript/themes/glitch/features/getting_started/index.js b/app/javascript/themes/glitch/features/getting_started/index.js deleted file mode 100644 index 74b019cf1..000000000 --- a/app/javascript/themes/glitch/features/getting_started/index.js +++ /dev/null @@ -1,145 +0,0 @@ -import React from 'react'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnLink from 'themes/glitch/features/ui/components/column_link'; -import ColumnSubheading from 'themes/glitch/features/ui/components/column_subheading'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { openModal } from 'themes/glitch/actions/modal'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, - home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, - notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, - public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, - navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' }, - settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, - community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, - direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, - follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, - sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, - blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, - info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, - show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' }, - pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, -}); - -const mapStateToProps = state => ({ - myAccount: state.getIn(['accounts', me]), - columns: state.getIn(['settings', 'columns']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class GettingStarted extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - myAccount: ImmutablePropTypes.map.isRequired, - columns: ImmutablePropTypes.list, - multiColumn: PropTypes.bool, - dispatch: PropTypes.func.isRequired, - }; - - openSettings = () => { - this.props.dispatch(openModal('SETTINGS', {})); - } - - openOnboardingModal = (e) => { - e.preventDefault(); - this.props.dispatch(openModal('ONBOARDING')); - } - - render () { - const { intl, myAccount, columns, multiColumn } = this.props; - - let navItems = []; - - if (multiColumn) { - if (!columns.find(item => item.get('id') === 'HOME')) { - navItems.push(<ColumnLink key='0' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />); - } - - if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) { - navItems.push(<ColumnLink key='1' icon='bell' text={intl.formatMessage(messages.notifications)} to='/notifications' />); - } - - if (!columns.find(item => item.get('id') === 'COMMUNITY')) { - navItems.push(<ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />); - } - - if (!columns.find(item => item.get('id') === 'PUBLIC')) { - navItems.push(<ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />); - } - } - - if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) { - navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />); - } - - navItems = navItems.concat([ - <ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, - <ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />, - ]); - - if (myAccount.get('locked')) { - navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />); - } - - navItems = navItems.concat([ - <ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />, - <ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />, - ]); - - return ( - <Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile> - <div className='scrollable optionally-scrollable'> - <div className='getting-started__wrapper'> - <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} /> - {navItems} - <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} /> - <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> - <ColumnLink icon='hand-o-right' text={intl.formatMessage(messages.show_me_around)} onClick={this.openOnboardingModal} /> - <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' /> - <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={this.openSettings} /> - <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' /> - </div> - - <div className='getting-started__footer'> - <div className='static-content getting-started'> - <p> - <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'> - <FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /> - </a> • - <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'> - <FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /> - </a> • - <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'> - <FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /> - </a> - </p> - <p> - <FormattedMessage - id='getting_started.open_source_notice' - defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.' - values={{ - github: <a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a>, - Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a>, - }} - /> - </p> - </div> - </div> - </div> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/hashtag_timeline/index.js b/app/javascript/themes/glitch/features/hashtag_timeline/index.js deleted file mode 100644 index a878931b3..000000000 --- a/app/javascript/themes/glitch/features/hashtag_timeline/index.js +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - refreshHashtagTimeline, - expandHashtagTimeline, -} from 'themes/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { FormattedMessage } from 'react-intl'; -import { connectHashtagStream } from 'themes/glitch/actions/streaming'; - -const mapStateToProps = (state, props) => ({ - hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0, -}); - -@connect(mapStateToProps) -export default class HashtagTimeline extends React.PureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - columnId: PropTypes.string, - dispatch: PropTypes.func.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('HASHTAG', { id: this.props.params.id })); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - _subscribe (dispatch, id) { - this.disconnect = dispatch(connectHashtagStream(id)); - } - - _unsubscribe () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - componentDidMount () { - const { dispatch } = this.props; - const { id } = this.props.params; - - dispatch(refreshHashtagTimeline(id)); - this._subscribe(dispatch, id); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.id !== this.props.params.id) { - this.props.dispatch(refreshHashtagTimeline(nextProps.params.id)); - this._unsubscribe(); - this._subscribe(this.props.dispatch, nextProps.params.id); - } - } - - componentWillUnmount () { - this._unsubscribe(); - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandHashtagTimeline(this.props.params.id)); - } - - render () { - const { hasUnread, columnId, multiColumn } = this.props; - const { id } = this.props.params; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='hashtag'> - <ColumnHeader - icon='hashtag' - active={hasUnread} - title={id} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - showBackButton - /> - - <StatusListContainer - trackScroll={!pinned} - scrollKey={`hashtag_timeline-${columnId}`} - timelineId={`hashtag:${id}`} - loadMore={this.handleLoadMore} - emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js b/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js deleted file mode 100644 index 533da6c36..000000000 --- a/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import SettingToggle from 'themes/glitch/features/notifications/components/setting_toggle'; -import SettingText from 'themes/glitch/components/setting_text'; - -const messages = defineMessages({ - filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, - settings: { id: 'home.settings', defaultMessage: 'Column settings' }, -}); - -@injectIntl -export default class ColumnSettings extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { settings, onChange, intl } = this.props; - - return ( - <div> - <span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} /> - </div> - - <div className='column-settings__row'> - <SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} /> - </div> - - <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> - - <div className='column-settings__row'> - <SettingText prefix='home_timeline' settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js b/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js deleted file mode 100644 index a0062f564..000000000 --- a/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnSettings from '../components/column_settings'; -import { changeSetting, saveSettings } from 'themes/glitch/actions/settings'; - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'home']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (key, checked) { - dispatch(changeSetting(['home', ...key], checked)); - }, - - onSave () { - dispatch(saveSettings()); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/themes/glitch/features/home_timeline/index.js b/app/javascript/themes/glitch/features/home_timeline/index.js deleted file mode 100644 index 8a65891cd..000000000 --- a/app/javascript/themes/glitch/features/home_timeline/index.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { expandHomeTimeline } from 'themes/glitch/actions/timelines'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { Link } from 'react-router-dom'; - -const messages = defineMessages({ - title: { id: 'column.home', defaultMessage: 'Home' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, -}); - -@connect(mapStateToProps) -@injectIntl -export default class HomeTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - columnId: PropTypes.string, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('HOME', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandHomeTimeline()); - } - - render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='home'> - <ColumnHeader - icon='home' - active={hasUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - <StatusListContainer - trackScroll={!pinned} - scrollKey={`home_timeline-${columnId}`} - loadMore={this.handleLoadMore} - timelineId='home' - emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/index.js b/app/javascript/themes/glitch/features/local_settings/index.js deleted file mode 100644 index 6c5d51413..000000000 --- a/app/javascript/themes/glitch/features/local_settings/index.js +++ /dev/null @@ -1,68 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; - -// Our imports -import LocalSettingsPage from './page'; -import LocalSettingsNavigation from './navigation'; -import { closeModal } from 'themes/glitch/actions/modal'; -import { changeLocalSetting } from 'themes/glitch/actions/local_settings'; - -// Stylesheet imports -import './style.scss'; - -const mapStateToProps = state => ({ - settings: state.get('local_settings'), -}); - -const mapDispatchToProps = dispatch => ({ - onChange (setting, value) { - dispatch(changeLocalSetting(setting, value)); - }, - onClose () { - dispatch(closeModal()); - }, -}); - -class LocalSettings extends React.PureComponent { - - static propTypes = { - onChange: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - settings: ImmutablePropTypes.map.isRequired, - }; - - state = { - currentIndex: 0, - }; - - navigateTo = (index) => - this.setState({ currentIndex: +index }); - - render () { - - const { navigateTo } = this; - const { onChange, onClose, settings } = this.props; - const { currentIndex } = this.state; - - return ( - <div className='glitch modal-root__modal local-settings'> - <LocalSettingsNavigation - index={currentIndex} - onClose={onClose} - onNavigate={navigateTo} - /> - <LocalSettingsPage - index={currentIndex} - onChange={onChange} - settings={settings} - /> - </div> - ); - } - -} - -export default connect(mapStateToProps, mapDispatchToProps)(LocalSettings); diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/index.js b/app/javascript/themes/glitch/features/local_settings/navigation/index.js deleted file mode 100644 index fa35e83c7..000000000 --- a/app/javascript/themes/glitch/features/local_settings/navigation/index.js +++ /dev/null @@ -1,74 +0,0 @@ -// Package imports -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, defineMessages } from 'react-intl'; - -// Our imports -import LocalSettingsNavigationItem from './item'; - -// Stylesheet imports -import './style.scss'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -const messages = defineMessages({ - general: { id: 'settings.general', defaultMessage: 'General' }, - collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' }, - media: { id: 'settings.media', defaultMessage: 'Media' }, - preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' }, - close: { id: 'settings.close', defaultMessage: 'Close' }, -}); - -@injectIntl -export default class LocalSettingsNavigation extends React.PureComponent { - - static propTypes = { - index : PropTypes.number, - intl : PropTypes.object.isRequired, - onClose : PropTypes.func.isRequired, - onNavigate : PropTypes.func.isRequired, - }; - - render () { - - const { index, intl, onClose, onNavigate } = this.props; - - return ( - <nav className='glitch local-settings__navigation'> - <LocalSettingsNavigationItem - active={index === 0} - index={0} - onNavigate={onNavigate} - title={intl.formatMessage(messages.general)} - /> - <LocalSettingsNavigationItem - active={index === 1} - index={1} - onNavigate={onNavigate} - title={intl.formatMessage(messages.collapsed)} - /> - <LocalSettingsNavigationItem - active={index === 2} - index={2} - onNavigate={onNavigate} - title={intl.formatMessage(messages.media)} - /> - <LocalSettingsNavigationItem - active={index === 3} - href='/settings/preferences' - index={3} - icon='cog' - title={intl.formatMessage(messages.preferences)} - /> - <LocalSettingsNavigationItem - active={index === 4} - className='close' - index={4} - onNavigate={onClose} - title={intl.formatMessage(messages.close)} - /> - </nav> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js b/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js deleted file mode 100644 index a352d5fb2..000000000 --- a/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js +++ /dev/null @@ -1,69 +0,0 @@ -// Package imports -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -// Stylesheet imports -import './style.scss'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -export default class LocalSettingsPage extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - className: PropTypes.string, - href: PropTypes.string, - icon: PropTypes.string, - index: PropTypes.number.isRequired, - onNavigate: PropTypes.func, - title: PropTypes.string, - }; - - handleClick = (e) => { - const { index, onNavigate } = this.props; - if (onNavigate) { - onNavigate(index); - e.preventDefault(); - } - } - - render () { - const { handleClick } = this; - const { - active, - className, - href, - icon, - onNavigate, - title, - } = this.props; - - const finalClassName = classNames('glitch', 'local-settings__navigation__item', { - active, - }, className); - - const iconElem = icon ? <i className={`fa fa-fw fa-${icon}`} /> : null; - - if (href) return ( - <a - href={href} - className={finalClassName} - > - {iconElem} {title} - </a> - ); - else if (onNavigate) return ( - <a - onClick={handleClick} - role='button' - tabIndex='0' - className={finalClassName} - > - {iconElem} {title} - </a> - ); - else return null; - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss b/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss deleted file mode 100644 index 7f7371993..000000000 --- a/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss +++ /dev/null @@ -1,27 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings__navigation__item { - display: block; - padding: 15px 20px; - color: inherit; - background: $primary-text-color; - border-bottom: 1px $ui-primary-color solid; - cursor: pointer; - text-decoration: none; - outline: none; - transition: background .3s; - - &:hover { - background: $ui-secondary-color; - } - - &.active { - background: $ui-highlight-color; - color: $primary-text-color; - } - - &.close, &.close:hover { - background: $error-value-color; - color: $primary-text-color; - } -} diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/style.scss b/app/javascript/themes/glitch/features/local_settings/navigation/style.scss deleted file mode 100644 index 0336f943b..000000000 --- a/app/javascript/themes/glitch/features/local_settings/navigation/style.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings__navigation { - background: $primary-text-color; - color: $ui-base-color; - width: 200px; - font-size: 15px; - line-height: 20px; - overflow-y: auto; -} diff --git a/app/javascript/themes/glitch/features/local_settings/page/index.js b/app/javascript/themes/glitch/features/local_settings/page/index.js deleted file mode 100644 index 498230f7b..000000000 --- a/app/javascript/themes/glitch/features/local_settings/page/index.js +++ /dev/null @@ -1,212 +0,0 @@ -// Package imports -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; - -// Our imports -import LocalSettingsPageItem from './item'; - -// Stylesheet imports -import './style.scss'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -const messages = defineMessages({ - layout_auto: { id: 'layout.auto', defaultMessage: 'Auto' }, - layout_desktop: { id: 'layout.desktop', defaultMessage: 'Desktop' }, - layout_mobile: { id: 'layout.single', defaultMessage: 'Mobile' }, - side_arm_none: { id: 'settings.side_arm.none', defaultMessage: 'None' }, -}); - -@injectIntl -export default class LocalSettingsPage extends React.PureComponent { - - static propTypes = { - index : PropTypes.number, - intl : PropTypes.object.isRequired, - onChange : PropTypes.func.isRequired, - settings : ImmutablePropTypes.map.isRequired, - }; - - pages = [ - ({ intl, onChange, settings }) => ( - <div className='glitch local-settings__page general'> - <h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1> - <LocalSettingsPageItem - settings={settings} - item={['layout']} - id='mastodon-settings--layout' - options={[ - { value: 'auto', message: intl.formatMessage(messages.layout_auto) }, - { value: 'multiple', message: intl.formatMessage(messages.layout_desktop) }, - { value: 'single', message: intl.formatMessage(messages.layout_mobile) }, - ]} - onChange={onChange} - > - <FormattedMessage id='settings.layout' defaultMessage='Layout:' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['stretch']} - id='mastodon-settings--stretch' - onChange={onChange} - > - <FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['navbar_under']} - id='mastodon-settings--navbar_under' - onChange={onChange} - > - <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' /> - </LocalSettingsPageItem> - <section> - <h2><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h2> - <LocalSettingsPageItem - settings={settings} - item={['side_arm']} - id='mastodon-settings--side_arm' - options={[ - { value: 'none', message: intl.formatMessage(messages.side_arm_none) }, - { value: 'direct', message: intl.formatMessage({ id: 'privacy.direct.short' }) }, - { value: 'private', message: intl.formatMessage({ id: 'privacy.private.short' }) }, - { value: 'unlisted', message: intl.formatMessage({ id: 'privacy.unlisted.short' }) }, - { value: 'public', message: intl.formatMessage({ id: 'privacy.public.short' }) }, - ]} - onChange={onChange} - > - <FormattedMessage id='settings.side_arm' defaultMessage='Secondary toot button:' /> - </LocalSettingsPageItem> - </section> - </div> - ), - ({ onChange, settings }) => ( - <div className='glitch local-settings__page collapsed'> - <h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'enabled']} - id='mastodon-settings--collapsed-enabled' - onChange={onChange} - > - <FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' /> - </LocalSettingsPageItem> - <section> - <h2><FormattedMessage id='settings.auto_collapse' defaultMessage='Automatic collapsing' /></h2> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'all']} - id='mastodon-settings--collapsed-auto-all' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - > - <FormattedMessage id='settings.auto_collapse_all' defaultMessage='Everything' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'notifications']} - id='mastodon-settings--collapsed-auto-notifications' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_notifications' defaultMessage='Notifications' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'lengthy']} - id='mastodon-settings--collapsed-auto-lengthy' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'reblogs']} - id='mastodon-settings--collapsed-auto-reblogs' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_reblogs' defaultMessage='Boosts' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'replies']} - id='mastodon-settings--collapsed-auto-replies' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_replies' defaultMessage='Replies' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'media']} - id='mastodon-settings--collapsed-auto-media' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_media' defaultMessage='Toots with media' /> - </LocalSettingsPageItem> - </section> - <section> - <h2><FormattedMessage id='settings.image_backgrounds' defaultMessage='Image backgrounds' /></h2> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'backgrounds', 'user_backgrounds']} - id='mastodon-settings--collapsed-user-backgrouns' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - > - <FormattedMessage id='settings.image_backgrounds_users' defaultMessage='Give collapsed toots an image background' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'backgrounds', 'preview_images']} - id='mastodon-settings--collapsed-preview-images' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - > - <FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' /> - </LocalSettingsPageItem> - </section> - </div> - ), - ({ onChange, settings }) => ( - <div className='glitch local-settings__page media'> - <h1><FormattedMessage id='settings.media' defaultMessage='Media' /></h1> - <LocalSettingsPageItem - settings={settings} - item={['media', 'letterbox']} - id='mastodon-settings--media-letterbox' - onChange={onChange} - > - <FormattedMessage id='settings.media_letterbox' defaultMessage='Letterbox media' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['media', 'fullwidth']} - id='mastodon-settings--media-fullwidth' - onChange={onChange} - > - <FormattedMessage id='settings.media_fullwidth' defaultMessage='Full-width media previews' /> - </LocalSettingsPageItem> - </div> - ), - ]; - - render () { - const { pages } = this; - const { index, intl, onChange, settings } = this.props; - const CurrentPage = pages[index] || pages[0]; - - return <CurrentPage intl={intl} onChange={onChange} settings={settings} />; - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/page/item/index.js b/app/javascript/themes/glitch/features/local_settings/page/item/index.js deleted file mode 100644 index 37e28c084..000000000 --- a/app/javascript/themes/glitch/features/local_settings/page/item/index.js +++ /dev/null @@ -1,90 +0,0 @@ -// Package imports -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -// Stylesheet imports -import './style.scss'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -export default class LocalSettingsPageItem extends React.PureComponent { - - static propTypes = { - children: PropTypes.element.isRequired, - dependsOn: PropTypes.array, - dependsOnNot: PropTypes.array, - id: PropTypes.string.isRequired, - item: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, - options: PropTypes.arrayOf(PropTypes.shape({ - value: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, - })), - settings: ImmutablePropTypes.map.isRequired, - }; - - handleChange = e => { - const { target } = e; - const { item, onChange, options } = this.props; - if (options && options.length > 0) onChange(item, target.value); - else onChange(item, target.checked); - } - - render () { - const { handleChange } = this; - const { settings, item, id, options, children, dependsOn, dependsOnNot } = this.props; - let enabled = true; - - if (dependsOn) { - for (let i = 0; i < dependsOn.length; i++) { - enabled = enabled && settings.getIn(dependsOn[i]); - } - } - if (dependsOnNot) { - for (let i = 0; i < dependsOnNot.length; i++) { - enabled = enabled && !settings.getIn(dependsOnNot[i]); - } - } - - if (options && options.length > 0) { - const currentValue = settings.getIn(item); - const optionElems = options && options.length > 0 && options.map((opt) => ( - <option - key={opt.value} - value={opt.value} - > - {opt.message} - </option> - )); - return ( - <label className='glitch local-settings__page__item' htmlFor={id}> - <p>{children}</p> - <p> - <select - id={id} - disabled={!enabled} - onBlur={handleChange} - onChange={handleChange} - value={currentValue} - > - {optionElems} - </select> - </p> - </label> - ); - } else return ( - <label className='glitch local-settings__page__item' htmlFor={id}> - <input - id={id} - type='checkbox' - checked={settings.getIn(item)} - onChange={handleChange} - disabled={!enabled} - /> - {children} - </label> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/page/item/style.scss b/app/javascript/themes/glitch/features/local_settings/page/item/style.scss deleted file mode 100644 index b2d8f7185..000000000 --- a/app/javascript/themes/glitch/features/local_settings/page/item/style.scss +++ /dev/null @@ -1,7 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings__page__item { - select { - margin-bottom: 5px; - } -} diff --git a/app/javascript/themes/glitch/features/local_settings/page/style.scss b/app/javascript/themes/glitch/features/local_settings/page/style.scss deleted file mode 100644 index e9eedcad0..000000000 --- a/app/javascript/themes/glitch/features/local_settings/page/style.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings__page { - display: block; - flex: auto; - padding: 15px 20px 15px 20px; - width: 360px; - overflow-y: auto; -} diff --git a/app/javascript/themes/glitch/features/local_settings/style.scss b/app/javascript/themes/glitch/features/local_settings/style.scss deleted file mode 100644 index 765294607..000000000 --- a/app/javascript/themes/glitch/features/local_settings/style.scss +++ /dev/null @@ -1,34 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings { - position: relative; - display: flex; - flex-direction: row; - background: $ui-secondary-color; - color: $ui-base-color; - border-radius: 8px; - height: 80vh; - width: 80vw; - max-width: 740px; - max-height: 450px; - overflow: hidden; - - label { - display: block; - } - - h1 { - font-size: 18px; - font-weight: 500; - line-height: 24px; - margin-bottom: 20px; - } - - h2 { - font-size: 15px; - font-weight: 500; - line-height: 20px; - margin-top: 20px; - margin-bottom: 10px; - } -} diff --git a/app/javascript/themes/glitch/features/mutes/index.js b/app/javascript/themes/glitch/features/mutes/index.js deleted file mode 100644 index 1158b8262..000000000 --- a/app/javascript/themes/glitch/features/mutes/index.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll-4'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import { fetchMutes, expandMutes } from 'themes/glitch/actions/mutes'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.mutes', defaultMessage: 'Muted users' }, -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'mutes', 'items']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class Mutes extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - }; - - componentWillMount () { - this.props.dispatch(fetchMutes()); - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandMutes()); - } - } - - render () { - const { intl, accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='mutes'> - <div className='scrollable mutes' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js b/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js deleted file mode 100644 index 22a10753f..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -export default class ClearColumnButton extends React.Component { - - static propTypes = { - onClick: PropTypes.func.isRequired, - }; - - render () { - return ( - <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.props.onClick}><i className='fa fa-eraser' /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/column_settings.js b/app/javascript/themes/glitch/features/notifications/components/column_settings.js deleted file mode 100644 index 88a29d4d3..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/column_settings.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import ClearColumnButton from './clear_column_button'; -import SettingToggle from './setting_toggle'; - -export default class ColumnSettings extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - pushSettings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - onSave: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - }; - - onPushChange = (key, checked) => { - this.props.onChange(['push', ...key], checked); - } - - render () { - const { settings, pushSettings, onChange, onClear } = this.props; - - const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; - const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; - const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; - - const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); - const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />; - const pushMeta = showPushSettings && <FormattedMessage id='notifications.column_settings.push_meta' defaultMessage='This device' />; - - return ( - <div> - <div className='column-settings__row'> - <ClearColumnButton onClick={onClear} /> - </div> - - <div role='group' aria-labelledby='notifications-follow'> - <span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} /> - {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} - <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} /> - <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} /> - </div> - </div> - - <div role='group' aria-labelledby='notifications-favourite'> - <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> - {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} - <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} /> - <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} /> - </div> - </div> - - <div role='group' aria-labelledby='notifications-mention'> - <span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} /> - {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} - <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} /> - <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} /> - </div> - </div> - - <div role='group' aria-labelledby='notifications-reblog'> - <span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> - {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} - <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} /> - <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} /> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/follow.js b/app/javascript/themes/glitch/features/notifications/components/follow.js deleted file mode 100644 index 96cfe83e6..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/follow.js +++ /dev/null @@ -1,98 +0,0 @@ -// Package imports. -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { HotKeys } from 'react-hotkeys'; - -// Our imports. -import Permalink from 'themes/glitch/components/permalink'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import NotificationOverlayContainer from '../containers/overlay_container'; - -export default class NotificationFollow extends ImmutablePureComponent { - - static propTypes = { - hidden: PropTypes.bool, - id: PropTypes.string.isRequired, - account: ImmutablePropTypes.map.isRequired, - notification: ImmutablePropTypes.map.isRequired, - }; - - handleMoveUp = () => { - const { notification, onMoveUp } = this.props; - onMoveUp(notification.get('id')); - } - - handleMoveDown = () => { - const { notification, onMoveDown } = this.props; - onMoveDown(notification.get('id')); - } - - handleOpen = () => { - 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); - } - - getHandlers () { - return { - moveUp: this.handleMoveUp, - moveDown: this.handleMoveDown, - open: this.handleOpen, - openProfile: this.handleOpenProfile, - mention: this.handleMention, - reply: this.handleMention, - }; - } - - render () { - const { account, notification, hidden } = this.props; - - // Links to the display name. - const displayName = account.get('display_name_html') || account.get('username'); - const link = ( - <Permalink - className='notification__display-name' - href={account.get('url')} - title={account.get('acct')} - to={`/accounts/${account.get('id')}`} - dangerouslySetInnerHTML={{ __html: displayName }} - /> - ); - - // Renders. - return ( - <HotKeys handlers={this.getHandlers()}> - <div className='notification notification-follow focusable' tabIndex='0'> - <div className='notification__message'> - <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-user-plus' /> - </div> - - <FormattedMessage - id='notification.follow' - defaultMessage='{name} followed you' - values={{ name: link }} - /> - </div> - - <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} /> - <NotificationOverlayContainer notification={notification} /> - </div> - </HotKeys> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/notification.js b/app/javascript/themes/glitch/features/notifications/components/notification.js deleted file mode 100644 index 47f5770bf..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/notification.js +++ /dev/null @@ -1,93 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Our imports, -import StatusContainer from 'themes/glitch/containers/status_container'; -import NotificationFollow from './follow'; - -export default 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, - }; - - render () { - const { - hidden, - notification, - onMoveDown, - onMoveUp, - onMention, - } = this.props; - - switch(notification.get('type')) { - case 'follow': - return ( - <NotificationFollow - hidden={hidden} - id={notification.get('id')} - account={notification.get('account')} - notification={notification} - onMoveDown={onMoveDown} - onMoveUp={onMoveUp} - onMention={onMention} - /> - ); - case 'mention': - return ( - <StatusContainer - containerId={notification.get('id')} - hidden={hidden} - id={notification.get('status')} - notification={notification} - onMoveDown={onMoveDown} - onMoveUp={onMoveUp} - onMention={onMention} - withDismiss - /> - ); - case 'favourite': - return ( - <StatusContainer - containerId={notification.get('id')} - hidden={hidden} - id={notification.get('status')} - account={notification.get('account')} - prepend='favourite' - muted - notification={notification} - onMoveDown={onMoveDown} - onMoveUp={onMoveUp} - onMention={onMention} - withDismiss - /> - ); - case 'reblog': - return ( - <StatusContainer - containerId={notification.get('id')} - hidden={hidden} - id={notification.get('status')} - account={notification.get('account')} - prepend='reblog' - muted - notification={notification} - onMoveDown={onMoveDown} - onMoveUp={onMoveUp} - onMention={onMention} - withDismiss - /> - ); - default: - return null; - } - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/overlay.js b/app/javascript/themes/glitch/features/notifications/components/overlay.js deleted file mode 100644 index e56f9c628..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/overlay.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Notification overlay - */ - - -// Package imports. -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - markForDeletion: { id: 'notification.markForDeletion', defaultMessage: 'Mark for deletion' }, -}); - -@injectIntl -export default class NotificationOverlay extends ImmutablePureComponent { - - static propTypes = { - notification : ImmutablePropTypes.map.isRequired, - onMarkForDelete : PropTypes.func.isRequired, - show : PropTypes.bool.isRequired, - intl : PropTypes.object.isRequired, - }; - - onToggleMark = () => { - const mark = !this.props.notification.get('markedForDelete'); - const id = this.props.notification.get('id'); - this.props.onMarkForDelete(id, mark); - } - - render () { - const { notification, show, intl } = this.props; - - const active = notification.get('markedForDelete'); - const label = intl.formatMessage(messages.markForDeletion); - - return show ? ( - <div - aria-label={label} - role='checkbox' - aria-checked={active} - tabIndex={0} - className={`notification__dismiss-overlay ${active ? 'active' : ''}`} - onClick={this.onToggleMark} - > - <div className='wrappy'> - <div className='ckbox' aria-hidden='true' title={label}> - {active ? (<i className='fa fa-check' />) : ''} - </div> - </div> - </div> - ) : null; - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js b/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js deleted file mode 100644 index 281359d2a..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; - -export default class SettingToggle extends React.PureComponent { - - static propTypes = { - prefix: PropTypes.string, - settings: ImmutablePropTypes.map.isRequired, - settingKey: PropTypes.array.isRequired, - label: PropTypes.node.isRequired, - meta: PropTypes.node, - onChange: PropTypes.func.isRequired, - } - - onChange = ({ target }) => { - this.props.onChange(this.props.settingKey, target.checked); - } - - render () { - const { prefix, settings, settingKey, label, meta } = this.props; - const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-'); - - return ( - <div className='setting-toggle'> - <Toggle id={id} checked={settings.getIn(settingKey)} onChange={this.onChange} onKeyDown={this.onKeyDown} /> - <label htmlFor={id} className='setting-toggle__label'>{label}</label> - {meta && <span className='setting-meta__label'>{meta}</span>} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js deleted file mode 100644 index ddc8495f4..000000000 --- a/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js +++ /dev/null @@ -1,44 +0,0 @@ -import { connect } from 'react-redux'; -import { defineMessages, injectIntl } from 'react-intl'; -import ColumnSettings from '../components/column_settings'; -import { changeSetting, saveSettings } from 'themes/glitch/actions/settings'; -import { clearNotifications } from 'themes/glitch/actions/notifications'; -import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'themes/glitch/actions/push_notifications'; -import { openModal } from 'themes/glitch/actions/modal'; - -const messages = defineMessages({ - clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, - clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }, -}); - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'notifications']), - pushSettings: state.get('push_notifications'), -}); - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onChange (key, checked) { - if (key[0] === 'push') { - dispatch(changePushNotifications(key.slice(1), checked)); - } else { - dispatch(changeSetting(['notifications', ...key], checked)); - } - }, - - onSave () { - dispatch(saveSettings()); - dispatch(savePushNotificationSettings()); - }, - - onClear () { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(clearNotifications()), - })); - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings)); diff --git a/app/javascript/themes/glitch/features/notifications/containers/notification_container.js b/app/javascript/themes/glitch/features/notifications/containers/notification_container.js deleted file mode 100644 index 033c61e3d..000000000 --- a/app/javascript/themes/glitch/features/notifications/containers/notification_container.js +++ /dev/null @@ -1,26 +0,0 @@ -// Package imports. -import { connect } from 'react-redux'; - -// Our imports. -import { makeGetNotification } from 'themes/glitch/selectors'; -import Notification from '../components/notification'; -import { mentionCompose } from 'themes/glitch/actions/compose'; - -const makeMapStateToProps = () => { - const getNotification = makeGetNotification(); - - const mapStateToProps = (state, props) => ({ - notification: getNotification(state, props.notification, props.accountId), - notifCleaning: state.getIn(['notifications', 'cleaningMode']), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = dispatch => ({ - onMention: (account, router) => { - dispatch(mentionCompose(account, router)); - }, -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(Notification); diff --git a/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js b/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js deleted file mode 100644 index 52649cdd7..000000000 --- a/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js +++ /dev/null @@ -1,18 +0,0 @@ -// Package imports. -import { connect } from 'react-redux'; - -// Our imports. -import NotificationOverlay from '../components/overlay'; -import { markNotificationForDelete } from 'themes/glitch/actions/notifications'; - -const mapDispatchToProps = dispatch => ({ - onMarkForDelete(id, yes) { - dispatch(markNotificationForDelete(id, yes)); - }, -}); - -const mapStateToProps = state => ({ - show: state.getIn(['notifications', 'cleaningMode']), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay); diff --git a/app/javascript/themes/glitch/features/notifications/index.js b/app/javascript/themes/glitch/features/notifications/index.js deleted file mode 100644 index 1ecde660a..000000000 --- a/app/javascript/themes/glitch/features/notifications/index.js +++ /dev/null @@ -1,193 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - enterNotificationClearingMode, - expandNotifications, - scrollTopNotifications, -} from 'themes/glitch/actions/notifications'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import NotificationContainer from './containers/notification_container'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { createSelector } from 'reselect'; -import { List as ImmutableList } from 'immutable'; -import { debounce } from 'lodash'; -import ScrollableList from 'themes/glitch/components/scrollable_list'; - -const messages = defineMessages({ - title: { id: 'column.notifications', defaultMessage: 'Notifications' }, -}); - -const getNotifications = createSelector([ - state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()), - state => state.getIn(['notifications', 'items']), -], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type')))); - -const mapStateToProps = state => ({ - notifications: getNotifications(state), - localSettings: state.get('local_settings'), - isLoading: state.getIn(['notifications', 'isLoading'], true), - isUnread: state.getIn(['notifications', 'unread']) > 0, - hasMore: !!state.getIn(['notifications', 'next']), - notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), -}); - -/* glitch */ -const mapDispatchToProps = dispatch => ({ - onEnterCleaningMode(yes) { - dispatch(enterNotificationClearingMode(yes)); - }, - dispatch, -}); - -@connect(mapStateToProps, mapDispatchToProps) -@injectIntl -export default class Notifications extends React.PureComponent { - - static propTypes = { - columnId: PropTypes.string, - notifications: ImmutablePropTypes.list.isRequired, - dispatch: PropTypes.func.isRequired, - shouldUpdateScroll: PropTypes.func, - intl: PropTypes.object.isRequired, - isLoading: PropTypes.bool, - isUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - hasMore: PropTypes.bool, - localSettings: ImmutablePropTypes.map, - notifCleaningActive: PropTypes.bool, - onEnterCleaningMode: PropTypes.func, - }; - - static defaultProps = { - trackScroll: true, - }; - - handleScrollToBottom = debounce(() => { - this.props.dispatch(scrollTopNotifications(false)); - this.props.dispatch(expandNotifications()); - }, 300, { leading: true }); - - handleScrollToTop = debounce(() => { - this.props.dispatch(scrollTopNotifications(true)); - }, 100); - - handleScroll = debounce(() => { - this.props.dispatch(scrollTopNotifications(false)); - }, 100); - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('NOTIFICATIONS', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setColumnRef = c => { - this.column = c; - } - - handleMoveUp = id => { - const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) - 1; - this._selectChild(elementIndex); - } - - handleMoveDown = id => { - const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) + 1; - this._selectChild(elementIndex); - } - - _selectChild (index) { - const element = this.column.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - element.focus(); - } - } - - render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props; - const pinned = !!columnId; - const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />; - - let scrollableContent = null; - - if (isLoading && this.scrollableContent) { - scrollableContent = this.scrollableContent; - } else if (notifications.size > 0 || hasMore) { - scrollableContent = notifications.map((item) => ( - <NotificationContainer - key={item.get('id')} - notification={item} - accountId={item.get('account')} - onMoveUp={this.handleMoveUp} - onMoveDown={this.handleMoveDown} - /> - )); - } else { - scrollableContent = null; - } - - this.scrollableContent = scrollableContent; - - const scrollContainer = ( - <ScrollableList - scrollKey={`notifications-${columnId}`} - trackScroll={!pinned} - isLoading={isLoading} - hasMore={hasMore} - emptyMessage={emptyMessage} - onScrollToBottom={this.handleScrollToBottom} - onScrollToTop={this.handleScrollToTop} - onScroll={this.handleScroll} - shouldUpdateScroll={shouldUpdateScroll} - > - {scrollableContent} - </ScrollableList> - ); - - return ( - <Column - ref={this.setColumnRef} - name='notifications' - extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null} - > - <ColumnHeader - icon='bell' - active={isUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - localSettings={this.props.localSettings} - notifCleaning - notifCleaningActive={this.props.notifCleaningActive} // this is used to toggle the header text - onEnterCleaningMode={this.props.onEnterCleaningMode} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - {scrollContainer} - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/pinned_statuses/index.js b/app/javascript/themes/glitch/features/pinned_statuses/index.js deleted file mode 100644 index 0a3997850..000000000 --- a/app/javascript/themes/glitch/features/pinned_statuses/index.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { fetchPinnedStatuses } from 'themes/glitch/actions/pin_statuses'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import StatusList from 'themes/glitch/components/status_list'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.pins', defaultMessage: 'Pinned toot' }, -}); - -const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'pins', 'items']), - hasMore: !!state.getIn(['status_lists', 'pins', 'next']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class PinnedStatuses extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - intl: PropTypes.object.isRequired, - hasMore: PropTypes.bool.isRequired, - }; - - componentWillMount () { - this.props.dispatch(fetchPinnedStatuses()); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - render () { - const { intl, statusIds, hasMore } = this.props; - - return ( - <Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> - <ColumnBackButtonSlim /> - <StatusList - statusIds={statusIds} - scrollKey='pinned_statuses' - hasMore={hasMore} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js b/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js deleted file mode 100644 index 0185a7724..000000000 --- a/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnSettings from 'themes/glitch/features/community_timeline/components/column_settings'; -import { changeSetting } from 'themes/glitch/actions/settings'; - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'public']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (key, checked) { - dispatch(changeSetting(['public', ...key], checked)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/themes/glitch/features/public_timeline/index.js b/app/javascript/themes/glitch/features/public_timeline/index.js deleted file mode 100644 index f5b3865af..000000000 --- a/app/javascript/themes/glitch/features/public_timeline/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - refreshPublicTimeline, - expandPublicTimeline, -} from 'themes/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectPublicStream } from 'themes/glitch/actions/streaming'; - -const messages = defineMessages({ - title: { id: 'column.public', defaultMessage: 'Federated timeline' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0, -}); - -@connect(mapStateToProps) -@injectIntl -export default class PublicTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - columnId: PropTypes.string, - multiColumn: PropTypes.bool, - hasUnread: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('PUBLIC', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - componentDidMount () { - const { dispatch } = this.props; - - dispatch(refreshPublicTimeline()); - this.disconnect = dispatch(connectPublicStream()); - } - - componentWillUnmount () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandPublicTimeline()); - } - - render () { - const { intl, columnId, hasUnread, multiColumn } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='federated'> - <ColumnHeader - icon='globe' - active={hasUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - <StatusListContainer - timelineId='public' - loadMore={this.handleLoadMore} - trackScroll={!pinned} - scrollKey={`public_timeline-${columnId}`} - emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/reblogs/index.js b/app/javascript/themes/glitch/features/reblogs/index.js deleted file mode 100644 index 8723f7c7c..000000000 --- a/app/javascript/themes/glitch/features/reblogs/index.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { fetchReblogs } from 'themes/glitch/actions/interactions'; -import { ScrollContainer } from 'react-router-scroll-4'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]), -}); - -@connect(mapStateToProps) -export default class Reblogs extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - }; - - componentWillMount () { - this.props.dispatch(fetchReblogs(this.props.params.statusId)); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchReblogs(nextProps.params.statusId)); - } - } - - render () { - const { accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='reblogs'> - <div className='scrollable reblogs'> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/report/components/status_check_box.js b/app/javascript/themes/glitch/features/report/components/status_check_box.js deleted file mode 100644 index cc9232201..000000000 --- a/app/javascript/themes/glitch/features/report/components/status_check_box.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; - -export default class StatusCheckBox extends React.PureComponent { - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - checked: PropTypes.bool, - onToggle: PropTypes.func.isRequired, - disabled: PropTypes.bool, - }; - - render () { - const { status, checked, onToggle, disabled } = this.props; - const content = { __html: status.get('contentHtml') }; - - if (status.get('reblog')) { - return null; - } - - return ( - <div className='status-check-box'> - <div - className='status__content' - dangerouslySetInnerHTML={content} - /> - - <div className='status-check-box-toggle'> - <Toggle checked={checked} onChange={onToggle} disabled={disabled} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js b/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js deleted file mode 100644 index 40d55fb3c..000000000 --- a/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux'; -import StatusCheckBox from '../components/status_check_box'; -import { toggleStatusReport } from 'themes/glitch/actions/reports'; -import { Set as ImmutableSet } from 'immutable'; - -const mapStateToProps = (state, { id }) => ({ - status: state.getIn(['statuses', id]), - checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id), -}); - -const mapDispatchToProps = (dispatch, { id }) => ({ - - onToggle (e) { - dispatch(toggleStatusReport(id, e.target.checked)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox); diff --git a/app/javascript/themes/glitch/features/standalone/compose/index.js b/app/javascript/themes/glitch/features/standalone/compose/index.js deleted file mode 100644 index 8a8118178..000000000 --- a/app/javascript/themes/glitch/features/standalone/compose/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ComposeFormContainer from 'themes/glitch/features/compose/containers/compose_form_container'; -import NotificationsContainer from 'themes/glitch/features/ui/containers/notifications_container'; -import LoadingBarContainer from 'themes/glitch/features/ui/containers/loading_bar_container'; -import ModalContainer from 'themes/glitch/features/ui/containers/modal_container'; - -export default class Compose extends React.PureComponent { - - render () { - return ( - <div> - <ComposeFormContainer /> - <NotificationsContainer /> - <ModalContainer /> - <LoadingBarContainer className='loading-bar' /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js b/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js deleted file mode 100644 index 7c56f264f..000000000 --- a/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import { - refreshHashtagTimeline, - expandHashtagTimeline, -} from 'themes/glitch/actions/timelines'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; - -@connect() -export default class HashtagTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - hashtag: PropTypes.string.isRequired, - }; - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - componentDidMount () { - const { dispatch, hashtag } = this.props; - - dispatch(refreshHashtagTimeline(hashtag)); - - this.polling = setInterval(() => { - dispatch(refreshHashtagTimeline(hashtag)); - }, 10000); - } - - componentWillUnmount () { - if (typeof this.polling !== 'undefined') { - clearInterval(this.polling); - this.polling = null; - } - } - - handleLoadMore = () => { - this.props.dispatch(expandHashtagTimeline(this.props.hashtag)); - } - - render () { - const { hashtag } = this.props; - - return ( - <Column ref={this.setRef}> - <ColumnHeader - icon='hashtag' - title={hashtag} - onClick={this.handleHeaderClick} - /> - - <StatusListContainer - trackScroll={false} - scrollKey='standalone_hashtag_timeline' - timelineId={`hashtag:${hashtag}`} - loadMore={this.handleLoadMore} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/standalone/public_timeline/index.js b/app/javascript/themes/glitch/features/standalone/public_timeline/index.js deleted file mode 100644 index b3fb55288..000000000 --- a/app/javascript/themes/glitch/features/standalone/public_timeline/index.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import { - refreshPublicTimeline, - expandPublicTimeline, -} from 'themes/glitch/actions/timelines'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' }, -}); - -@connect() -@injectIntl -export default class PublicTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - componentDidMount () { - const { dispatch } = this.props; - - dispatch(refreshPublicTimeline()); - - this.polling = setInterval(() => { - dispatch(refreshPublicTimeline()); - }, 3000); - } - - componentWillUnmount () { - if (typeof this.polling !== 'undefined') { - clearInterval(this.polling); - this.polling = null; - } - } - - handleLoadMore = () => { - this.props.dispatch(expandPublicTimeline()); - } - - render () { - const { intl } = this.props; - - return ( - <Column ref={this.setRef}> - <ColumnHeader - icon='globe' - title={intl.formatMessage(messages.title)} - onClick={this.handleHeaderClick} - /> - - <StatusListContainer - timelineId='public' - loadMore={this.handleLoadMore} - scrollKey='standalone_public_timeline' - trackScroll={false} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/status/components/action_bar.js b/app/javascript/themes/glitch/features/status/components/action_bar.js deleted file mode 100644 index 6cda988d1..000000000 --- a/app/javascript/themes/glitch/features/status/components/action_bar.js +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import IconButton from 'themes/glitch/components/icon_button'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container'; -import { defineMessages, injectIntl } from 'react-intl'; -import { me } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, - cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, - favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' }, - share: { id: 'status.share', defaultMessage: 'Share' }, - pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, - unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, - embed: { id: 'status.embed', defaultMessage: 'Embed' }, -}); - -@injectIntl -export default class ActionBar extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReply: PropTypes.func.isRequired, - onReblog: PropTypes.func.isRequired, - onFavourite: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReport: PropTypes.func, - onPin: PropTypes.func, - onEmbed: PropTypes.func, - intl: PropTypes.object.isRequired, - }; - - handleReplyClick = () => { - this.props.onReply(this.props.status); - } - - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); - } - - handleFavouriteClick = () => { - this.props.onFavourite(this.props.status); - } - - handleDeleteClick = () => { - this.props.onDelete(this.props.status); - } - - handleMentionClick = () => { - this.props.onMention(this.props.status.get('account'), this.context.router.history); - } - - handleReport = () => { - this.props.onReport(this.props.status); - } - - handlePinClick = () => { - this.props.onPin(this.props.status); - } - - handleShare = () => { - navigator.share({ - text: this.props.status.get('search_index'), - url: this.props.status.get('url'), - }); - } - - handleEmbed = () => { - this.props.onEmbed(this.props.status); - } - - render () { - const { status, intl } = this.props; - - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - - let menu = []; - - if (publicStatus) { - menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); - } - - if (me === status.getIn(['account', 'id'])) { - if (publicStatus) { - menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - } - - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - } - - const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( - <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div> - ); - - let reblogIcon = 'retweet'; - //if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; - // else if (status.get('visibility') === 'private') reblogIcon = 'lock'; - - let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private'); - - return ( - <div className='detailed-status__action-bar'> - <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div> - <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> - <div className='detailed-status__button'><IconButton animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> - {shareButton} - - <div className='detailed-status__action-bar-dropdown'> - <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/status/components/card.js b/app/javascript/themes/glitch/features/status/components/card.js deleted file mode 100644 index bb83374b9..000000000 --- a/app/javascript/themes/glitch/features/status/components/card.js +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import punycode from 'punycode'; -import classnames from 'classnames'; - -const IDNA_PREFIX = 'xn--'; - -const decodeIDNA = domain => { - return domain - .split('.') - .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part) - .join('.'); -}; - -const getHostname = url => { - const parser = document.createElement('a'); - parser.href = url; - return parser.hostname; -}; - -export default class Card extends React.PureComponent { - - static propTypes = { - card: ImmutablePropTypes.map, - maxDescription: PropTypes.number, - }; - - static defaultProps = { - maxDescription: 50, - }; - - state = { - width: 0, - }; - - renderLink () { - const { card, maxDescription } = this.props; - - let image = ''; - let provider = card.get('provider_name'); - - if (card.get('image')) { - image = ( - <div className='status-card__image'> - <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} /> - </div> - ); - } - - if (provider.length < 1) { - provider = decodeIDNA(getHostname(card.get('url'))); - } - - const className = classnames('status-card', { - 'horizontal': card.get('width') > card.get('height'), - }); - - return ( - <a href={card.get('url')} className={className} target='_blank' rel='noopener'> - {image} - - <div className='status-card__content'> - <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> - <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p> - <span className='status-card__host'>{provider}</span> - </div> - </a> - ); - } - - renderPhoto () { - const { card } = this.props; - - return ( - <a href={card.get('url')} className='status-card-photo' target='_blank' rel='noopener'> - <img src={card.get('url')} alt={card.get('title')} width={card.get('width')} height={card.get('height')} /> - </a> - ); - } - - setRef = c => { - if (c) { - this.setState({ width: c.offsetWidth }); - } - } - - renderVideo () { - const { card } = this.props; - const content = { __html: card.get('html') }; - const { width } = this.state; - const ratio = card.get('width') / card.get('height'); - const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio); - - return ( - <div - ref={this.setRef} - className='status-card-video' - dangerouslySetInnerHTML={content} - style={{ height }} - /> - ); - } - - render () { - const { card } = this.props; - - if (card === null) { - return null; - } - - switch(card.get('type')) { - case 'link': - return this.renderLink(); - case 'photo': - return this.renderPhoto(); - case 'video': - return this.renderVideo(); - case 'rich': - default: - return null; - } - } - -} diff --git a/app/javascript/themes/glitch/features/status/components/detailed_status.js b/app/javascript/themes/glitch/features/status/components/detailed_status.js deleted file mode 100644 index df78ce4b6..000000000 --- a/app/javascript/themes/glitch/features/status/components/detailed_status.js +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from 'themes/glitch/components/avatar'; -import DisplayName from 'themes/glitch/components/display_name'; -import StatusContent from 'themes/glitch/components/status_content'; -import StatusGallery from 'themes/glitch/components/media_gallery'; -import AttachmentList from 'themes/glitch/components/attachment_list'; -import { Link } from 'react-router-dom'; -import { FormattedDate, FormattedNumber } from 'react-intl'; -import CardContainer from '../containers/card_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Video from 'themes/glitch/features/video'; -import VisibilityIcon from 'themes/glitch/components/status_visibility_icon'; - -export default class DetailedStatus extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - settings: ImmutablePropTypes.map.isRequired, - onOpenMedia: PropTypes.func.isRequired, - onOpenVideo: PropTypes.func.isRequired, - }; - - handleAccountClick = (e) => { - if (e.button === 0) { - e.preventDefault(); - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - - e.stopPropagation(); - } - - // handleOpenVideo = startTime => { - // this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); - // } - - render () { - const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; - const { expanded, setExpansion, settings } = this.props; - - let media = ''; - let mediaIcon = null; - let applicationLink = ''; - let reblogLink = ''; - let reblogIcon = 'retweet'; - - if (status.get('media_attachments').size > 0) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - media = <AttachmentList media={status.get('media_attachments')} />; - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = ( - <Video - sensitive={status.get('sensitive')} - media={status.getIn(['media_attachments', 0])} - letterbox={settings.getIn(['media', 'letterbox'])} - fullwidth={settings.getIn(['media', 'fullwidth'])} - onOpenVideo={this.props.onOpenVideo} - autoplay - /> - ); - mediaIcon = 'video-camera'; - } else { - media = ( - <StatusGallery - sensitive={status.get('sensitive')} - media={status.get('media_attachments')} - letterbox={settings.getIn(['media', 'letterbox'])} - onOpenMedia={this.props.onOpenMedia} - /> - ); - mediaIcon = 'picture-o'; - } - } else media = <CardContainer statusId={status.get('id')} />; - - if (status.get('application')) { - applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>; - } - - if (status.get('visibility') === 'direct') { - reblogIcon = 'envelope'; - } else if (status.get('visibility') === 'private') { - reblogIcon = 'lock'; - } - - if (status.get('visibility') === 'private') { - reblogLink = <i className={`fa fa-${reblogIcon}`} />; - } else { - reblogLink = (<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> - <i className={`fa fa-${reblogIcon}`} /> - <span className='detailed-status__reblogs'> - <FormattedNumber value={status.get('reblogs_count')} /> - </span> - </Link>); - } - - return ( - <div className='detailed-status'> - <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')} /> - </a> - - <StatusContent - status={status} - media={media} - mediaIcon={mediaIcon} - expanded={expanded} - setExpansion={setExpansion} - /> - - <div className='detailed-status__meta'> - <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> - <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> - </a>{applicationLink} · {reblogLink} · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> - <i className='fa fa-star' /> - <span className='detailed-status__favorites'> - <FormattedNumber value={status.get('favourites_count')} /> - </span> - </Link> · <VisibilityIcon visibility={status.get('visibility')} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/status/containers/card_container.js b/app/javascript/themes/glitch/features/status/containers/card_container.js deleted file mode 100644 index a97404de1..000000000 --- a/app/javascript/themes/glitch/features/status/containers/card_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import Card from '../components/card'; - -const mapStateToProps = (state, { statusId }) => ({ - card: state.getIn(['cards', statusId], null), -}); - -export default connect(mapStateToProps)(Card); diff --git a/app/javascript/themes/glitch/features/status/index.js b/app/javascript/themes/glitch/features/status/index.js deleted file mode 100644 index 8561bd4de..000000000 --- a/app/javascript/themes/glitch/features/status/index.js +++ /dev/null @@ -1,358 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { fetchStatus } from 'themes/glitch/actions/statuses'; -import MissingIndicator from 'themes/glitch/components/missing_indicator'; -import DetailedStatus from './components/detailed_status'; -import ActionBar from './components/action_bar'; -import Column from 'themes/glitch/features/ui/components/column'; -import { - favourite, - unfavourite, - reblog, - unreblog, - pin, - unpin, -} from 'themes/glitch/actions/interactions'; -import { - replyCompose, - mentionCompose, -} from 'themes/glitch/actions/compose'; -import { deleteStatus } from 'themes/glitch/actions/statuses'; -import { initReport } from 'themes/glitch/actions/reports'; -import { makeGetStatus } from 'themes/glitch/selectors'; -import { ScrollContainer } from 'react-router-scroll-4'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import StatusContainer from 'themes/glitch/containers/status_container'; -import { openModal } from 'themes/glitch/actions/modal'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { HotKeys } from 'react-hotkeys'; -import { boostModal, deleteModal } from 'themes/glitch/util/initial_state'; -import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'themes/glitch/util/fullscreen'; - -const messages = defineMessages({ - deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, - deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, -}); - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = (state, props) => ({ - status: getStatus(state, props.params.statusId), - settings: state.get('local_settings'), - ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]), - descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]), - }); - - return mapStateToProps; -}; - -@injectIntl -@connect(makeMapStateToProps) -export default class Status extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - status: ImmutablePropTypes.map, - settings: ImmutablePropTypes.map.isRequired, - ancestorsIds: ImmutablePropTypes.list, - descendantsIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - }; - - state = { - fullscreen: false, - isExpanded: null, - }; - - componentWillMount () { - this.props.dispatch(fetchStatus(this.props.params.statusId)); - } - - componentDidMount () { - attachFullscreenListener(this.onFullScreenChange); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this._scrolledIntoView = false; - this.props.dispatch(fetchStatus(nextProps.params.statusId)); - } - } - - handleExpandedToggle = () => { - if (this.props.status.get('spoiler_text')) { - this.setExpansion(this.state.isExpanded ? null : true); - } - }; - - handleFavouriteClick = (status) => { - if (status.get('favourited')) { - this.props.dispatch(unfavourite(status)); - } else { - this.props.dispatch(favourite(status)); - } - } - - handlePin = (status) => { - if (status.get('pinned')) { - this.props.dispatch(unpin(status)); - } else { - this.props.dispatch(pin(status)); - } - } - - handleReplyClick = (status) => { - this.props.dispatch(replyCompose(status, this.context.router.history)); - } - - handleModalReblog = (status) => { - this.props.dispatch(reblog(status)); - } - - handleReblogClick = (status, e) => { - if (status.get('reblogged')) { - this.props.dispatch(unreblog(status)); - } else { - if (e.shiftKey || !boostModal) { - this.handleModalReblog(status); - } else { - this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); - } - } - } - - handleDeleteClick = (status) => { - const { dispatch, intl } = this.props; - - if (!deleteModal) { - dispatch(deleteStatus(status.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'))), - })); - } - } - - handleMentionClick = (account, router) => { - this.props.dispatch(mentionCompose(account, router)); - } - - handleOpenMedia = (media, index) => { - this.props.dispatch(openModal('MEDIA', { media, index })); - } - - handleOpenVideo = (media, time) => { - this.props.dispatch(openModal('VIDEO', { media, time })); - } - - handleReport = (status) => { - this.props.dispatch(initReport(status.get('account'), status)); - } - - handleEmbed = (status) => { - this.props.dispatch(openModal('EMBED', { url: status.get('url') })); - } - - handleHotkeyMoveUp = () => { - this.handleMoveUp(this.props.status.get('id')); - } - - handleHotkeyMoveDown = () => { - this.handleMoveDown(this.props.status.get('id')); - } - - handleHotkeyReply = e => { - e.preventDefault(); - this.handleReplyClick(this.props.status); - } - - handleHotkeyFavourite = () => { - this.handleFavouriteClick(this.props.status); - } - - handleHotkeyBoost = () => { - this.handleReblogClick(this.props.status); - } - - handleHotkeyMention = e => { - e.preventDefault(); - this.handleMentionClick(this.props.status); - } - - handleHotkeyOpenProfile = () => { - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - - handleMoveUp = id => { - const { status, ancestorsIds, descendantsIds } = this.props; - - if (id === status.get('id')) { - this._selectChild(ancestorsIds.size - 1); - } else { - let index = ancestorsIds.indexOf(id); - - if (index === -1) { - index = descendantsIds.indexOf(id); - this._selectChild(ancestorsIds.size + index); - } else { - this._selectChild(index - 1); - } - } - } - - handleMoveDown = id => { - const { status, ancestorsIds, descendantsIds } = this.props; - - if (id === status.get('id')) { - this._selectChild(ancestorsIds.size + 1); - } else { - let index = ancestorsIds.indexOf(id); - - if (index === -1) { - index = descendantsIds.indexOf(id); - this._selectChild(ancestorsIds.size + index + 2); - } else { - this._selectChild(index + 1); - } - } - } - - _selectChild (index) { - const element = this.node.querySelectorAll('.focusable')[index]; - - if (element) { - element.focus(); - } - } - - renderChildren (list) { - return list.map(id => ( - <StatusContainer - key={id} - id={id} - onMoveUp={this.handleMoveUp} - onMoveDown={this.handleMoveDown} - /> - )); - } - - setExpansion = value => { - this.setState({ isExpanded: value ? true : null }); - } - - setRef = c => { - this.node = c; - } - - componentDidUpdate () { - if (this._scrolledIntoView) { - return; - } - - const { status, ancestorsIds } = this.props; - - if (status && ancestorsIds && ancestorsIds.size > 0) { - const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1]; - - if (element) { - element.scrollIntoView(true); - this._scrolledIntoView = true; - } - } - } - - componentWillUnmount () { - detachFullscreenListener(this.onFullScreenChange); - } - - onFullScreenChange = () => { - this.setState({ fullscreen: isFullscreen() }); - } - - render () { - let ancestors, descendants; - const { setExpansion } = this; - const { status, settings, ancestorsIds, descendantsIds } = this.props; - const { fullscreen, isExpanded } = this.state; - - if (status === null) { - return ( - <Column> - <ColumnBackButton /> - <MissingIndicator /> - </Column> - ); - } - - if (ancestorsIds && ancestorsIds.size > 0) { - ancestors = <div>{this.renderChildren(ancestorsIds)}</div>; - } - - if (descendantsIds && descendantsIds.size > 0) { - descendants = <div>{this.renderChildren(descendantsIds)}</div>; - } - - const handlers = { - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - openProfile: this.handleHotkeyOpenProfile, - toggleSpoiler: this.handleExpandedToggle, - }; - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='thread'> - <div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}> - {ancestors} - - <HotKeys handlers={handlers}> - <div className='focusable' tabIndex='0'> - <DetailedStatus - status={status} - settings={settings} - onOpenVideo={this.handleOpenVideo} - onOpenMedia={this.handleOpenMedia} - expanded={isExpanded} - setExpansion={setExpansion} - /> - - <ActionBar - status={status} - onReply={this.handleReplyClick} - onFavourite={this.handleFavouriteClick} - onReblog={this.handleReblogClick} - onDelete={this.handleDeleteClick} - onMention={this.handleMentionClick} - onReport={this.handleReport} - onPin={this.handlePin} - onEmbed={this.handleEmbed} - /> - </div> - </HotKeys> - - {descendants} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/actions_modal.js b/app/javascript/themes/glitch/features/ui/components/actions_modal.js deleted file mode 100644 index 7a2b78b63..000000000 --- a/app/javascript/themes/glitch/features/ui/components/actions_modal.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import StatusContent from 'themes/glitch/components/status_content'; -import Avatar from 'themes/glitch/components/avatar'; -import RelativeTimestamp from 'themes/glitch/components/relative_timestamp'; -import DisplayName from 'themes/glitch/components/display_name'; -import IconButton from 'themes/glitch/components/icon_button'; -import classNames from 'classnames'; - -export default class ActionsModal extends ImmutablePureComponent { - - static propTypes = { - status: ImmutablePropTypes.map, - actions: PropTypes.array, - onClick: PropTypes.func, - }; - - renderAction = (action, i) => { - if (action === null) { - return <li key={`sep-${i}`} className='dropdown-menu__separator' />; - } - - const { icon = null, text, meta = null, active = false, href = '#' } = action; - - return ( - <li key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}> - {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' />} - <div> - <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div> - <div>{meta}</div> - </div> - </a> - </li> - ); - } - - render () { - const status = this.props.status && ( - <div className='status light'> - <div className='boost-modal__status-header'> - <div className='boost-modal__status-time'> - <a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> - <RelativeTimestamp timestamp={this.props.status.get('created_at')} /> - </a> - </div> - - <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} /> - </div> - - <DisplayName account={this.props.status.get('account')} /> - </a> - </div> - - <StatusContent status={this.props.status} /> - </div> - ); - - return ( - <div className='modal-root__modal actions-modal'> - {status} - - <ul> - {this.props.actions.map(this.renderAction)} - </ul> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/boost_modal.js b/app/javascript/themes/glitch/features/ui/components/boost_modal.js deleted file mode 100644 index 49781db10..000000000 --- a/app/javascript/themes/glitch/features/ui/components/boost_modal.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Button from 'themes/glitch/components/button'; -import StatusContent from 'themes/glitch/components/status_content'; -import Avatar from 'themes/glitch/components/avatar'; -import RelativeTimestamp from 'themes/glitch/components/relative_timestamp'; -import DisplayName from 'themes/glitch/components/display_name'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, -}); - -@injectIntl -export default class BoostModal extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReblog: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - componentDidMount() { - this.button.focus(); - } - - handleReblog = () => { - this.props.onReblog(this.props.status); - this.props.onClose(); - } - - handleAccountClick = (e) => { - if (e.button === 0) { - e.preventDefault(); - 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; - - return ( - <div className='modal-root__modal boost-modal'> - <div className='boost-modal__container'> - <div className='status light'> - <div className='boost-modal__status-header'> - <div className='boost-modal__status-time'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> - </div> - - <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'> - <div className='status__avatar'> - <Avatar account={status.get('account')} size={48} /> - </div> - - <DisplayName account={status.get('account')} /> - </a> - </div> - - <StatusContent status={status} /> - </div> - </div> - - <div className='boost-modal__action-bar'> - <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div> - <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/bundle.js b/app/javascript/themes/glitch/features/ui/components/bundle.js deleted file mode 100644 index fc88e0c70..000000000 --- a/app/javascript/themes/glitch/features/ui/components/bundle.js +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const emptyComponent = () => null; -const noop = () => { }; - -class Bundle extends React.Component { - - static propTypes = { - fetchComponent: PropTypes.func.isRequired, - loading: PropTypes.func, - error: PropTypes.func, - children: PropTypes.func.isRequired, - renderDelay: PropTypes.number, - onFetch: PropTypes.func, - onFetchSuccess: PropTypes.func, - onFetchFail: PropTypes.func, - } - - static defaultProps = { - loading: emptyComponent, - error: emptyComponent, - renderDelay: 0, - onFetch: noop, - onFetchSuccess: noop, - onFetchFail: noop, - } - - static cache = {} - - state = { - mod: undefined, - forceRender: false, - } - - componentWillMount() { - this.load(this.props); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.fetchComponent !== this.props.fetchComponent) { - this.load(nextProps); - } - } - - componentWillUnmount () { - if (this.timeout) { - clearTimeout(this.timeout); - } - } - - load = (props) => { - const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; - - onFetch(); - - if (Bundle.cache[fetchComponent.name]) { - const mod = Bundle.cache[fetchComponent.name]; - - this.setState({ mod: mod.default }); - onFetchSuccess(); - return Promise.resolve(); - } - - this.setState({ mod: undefined }); - - if (renderDelay !== 0) { - this.timestamp = new Date(); - this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay); - } - - return fetchComponent() - .then((mod) => { - Bundle.cache[fetchComponent.name] = mod; - this.setState({ mod: mod.default }); - onFetchSuccess(); - }) - .catch((error) => { - this.setState({ mod: null }); - onFetchFail(error); - }); - } - - render() { - const { loading: Loading, error: Error, children, renderDelay } = this.props; - const { mod, forceRender } = this.state; - const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay; - - if (mod === undefined) { - return (elapsed >= renderDelay || forceRender) ? <Loading /> : null; - } - - if (mod === null) { - return <Error onRetry={this.load} />; - } - - return children(mod); - } - -} - -export default Bundle; diff --git a/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js b/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js deleted file mode 100644 index daedc6299..000000000 --- a/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; - -import Column from './column'; -import ColumnHeader from './column_header'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import IconButton from 'themes/glitch/components/icon_button'; - -const messages = defineMessages({ - title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, - body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' }, - retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, -}); - -class BundleColumnError extends React.Component { - - static propTypes = { - onRetry: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - } - - handleRetry = () => { - this.props.onRetry(); - } - - render () { - const { intl: { formatMessage } } = this.props; - - return ( - <Column> - <ColumnHeader icon='exclamation-circle' type={formatMessage(messages.title)} /> - <ColumnBackButtonSlim /> - <div className='error-column'> - <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} /> - {formatMessage(messages.body)} - </div> - </Column> - ); - } - -} - -export default injectIntl(BundleColumnError); diff --git a/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js b/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js deleted file mode 100644 index 8cca32ae9..000000000 --- a/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; - -import IconButton from 'themes/glitch/components/icon_button'; - -const messages = defineMessages({ - error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' }, - retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' }, - close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' }, -}); - -class BundleModalError extends React.Component { - - static propTypes = { - onRetry: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - } - - handleRetry = () => { - this.props.onRetry(); - } - - render () { - const { onClose, intl: { formatMessage } } = this.props; - - // Keep the markup in sync with <ModalLoading /> - // (make sure they have the same dimensions) - return ( - <div className='modal-root__modal error-modal'> - <div className='error-modal__body'> - <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} /> - {formatMessage(messages.error)} - </div> - - <div className='error-modal__footer'> - <div> - <button - onClick={onClose} - className='error-modal__nav onboarding-modal__skip' - > - {formatMessage(messages.close)} - </button> - </div> - </div> - </div> - ); - } - -} - -export default injectIntl(BundleModalError); diff --git a/app/javascript/themes/glitch/features/ui/components/column.js b/app/javascript/themes/glitch/features/ui/components/column.js deleted file mode 100644 index 73a5bc15e..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import ColumnHeader from './column_header'; -import PropTypes from 'prop-types'; -import { debounce } from 'lodash'; -import { scrollTop } from 'themes/glitch/util/scroll'; -import { isMobile } from 'themes/glitch/util/is_mobile'; - -export default class Column extends React.PureComponent { - - static propTypes = { - heading: PropTypes.string, - icon: PropTypes.string, - children: PropTypes.node, - active: PropTypes.bool, - hideHeadingOnMobile: PropTypes.bool, - name: PropTypes.string, - }; - - handleHeaderClick = () => { - const scrollable = this.node.querySelector('.scrollable'); - - if (!scrollable) { - return; - } - - this._interruptScrollAnimation = scrollTop(scrollable); - } - - scrollTop () { - const scrollable = this.node.querySelector('.scrollable'); - - if (!scrollable) { - return; - } - - this._interruptScrollAnimation = scrollTop(scrollable); - } - - - handleScroll = debounce(() => { - if (typeof this._interruptScrollAnimation !== 'undefined') { - this._interruptScrollAnimation(); - } - }, 200) - - setRef = (c) => { - this.node = c; - } - - render () { - const { heading, icon, children, active, hideHeadingOnMobile, name } = this.props; - - const showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth))); - - const columnHeaderId = showHeading && heading.replace(/ /g, '-'); - const header = showHeading && ( - <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} columnHeaderId={columnHeaderId} /> - ); - return ( - <div - ref={this.setRef} - role='region' - data-column={name} - aria-labelledby={columnHeaderId} - className='column' - onScroll={this.handleScroll} - > - {header} - {children} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/column_header.js b/app/javascript/themes/glitch/features/ui/components/column_header.js deleted file mode 100644 index af195ea9c..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column_header.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class ColumnHeader extends React.PureComponent { - - static propTypes = { - icon: PropTypes.string, - type: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func, - columnHeaderId: PropTypes.string, - }; - - handleClick = () => { - this.props.onClick(); - } - - render () { - const { type, active, columnHeaderId } = this.props; - - let icon = ''; - - if (this.props.icon) { - icon = <i className={`fa fa-fw fa-${this.props.icon} column-header__icon`} />; - } - - return ( - <div role='heading' tabIndex='0' className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}> - {icon} - {type} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/column_link.js b/app/javascript/themes/glitch/features/ui/components/column_link.js deleted file mode 100644 index b845d1895..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column_link.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -const ColumnLink = ({ icon, text, to, onClick, href, method }) => { - if (href) { - return ( - <a href={href} className='column-link' data-method={method}> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> - {text} - </a> - ); - } else if (to) { - return ( - <Link to={to} className='column-link'> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> - {text} - </Link> - ); - } else { - return ( - <a onClick={onClick} className='column-link' role='button' tabIndex='0' data-method={method}> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> - {text} - </a> - ); - } -}; - -ColumnLink.propTypes = { - icon: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - to: PropTypes.string, - onClick: PropTypes.func, - href: PropTypes.string, - method: PropTypes.string, -}; - -export default ColumnLink; diff --git a/app/javascript/themes/glitch/features/ui/components/column_loading.js b/app/javascript/themes/glitch/features/ui/components/column_loading.js deleted file mode 100644 index 75f26218a..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column_loading.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class ColumnLoading extends ImmutablePureComponent { - - static propTypes = { - title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), - icon: PropTypes.string, - }; - - static defaultProps = { - title: '', - icon: '', - }; - - render() { - let { title, icon } = this.props; - return ( - <Column> - <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} /> - <div className='scrollable' /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/column_subheading.js b/app/javascript/themes/glitch/features/ui/components/column_subheading.js deleted file mode 100644 index 8160c4aa3..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column_subheading.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const ColumnSubheading = ({ text }) => { - return ( - <div className='column-subheading'> - {text} - </div> - ); -}; - -ColumnSubheading.propTypes = { - text: PropTypes.string.isRequired, -}; - -export default ColumnSubheading; diff --git a/app/javascript/themes/glitch/features/ui/components/columns_area.js b/app/javascript/themes/glitch/features/ui/components/columns_area.js deleted file mode 100644 index 452950363..000000000 --- a/app/javascript/themes/glitch/features/ui/components/columns_area.js +++ /dev/null @@ -1,174 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl } from 'react-intl'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import ReactSwipeableViews from 'react-swipeable-views'; -import { links, getIndex, getLink } from './tabs_bar'; - -import BundleContainer from '../containers/bundle_container'; -import ColumnLoading from './column_loading'; -import DrawerLoading from './drawer_loading'; -import BundleColumnError from './bundle_column_error'; -import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from 'themes/glitch/util/async-components'; - -import detectPassiveEvents from 'detect-passive-events'; -import { scrollRight } from 'themes/glitch/util/scroll'; - -const componentMap = { - 'COMPOSE': Compose, - 'HOME': HomeTimeline, - 'NOTIFICATIONS': Notifications, - 'PUBLIC': PublicTimeline, - 'COMMUNITY': CommunityTimeline, - 'HASHTAG': HashtagTimeline, - 'DIRECT': DirectTimeline, - 'FAVOURITES': FavouritedStatuses, -}; - -@component => injectIntl(component, { withRef: true }) -export default class ColumnsArea extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object.isRequired, - }; - - static propTypes = { - intl: PropTypes.object.isRequired, - columns: ImmutablePropTypes.list.isRequired, - singleColumn: PropTypes.bool, - children: PropTypes.node, - }; - - state = { - shouldAnimate: false, - } - - componentWillReceiveProps() { - this.setState({ shouldAnimate: false }); - } - - componentDidMount() { - if (!this.props.singleColumn) { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - this.lastIndex = getIndex(this.context.router.history.location.pathname); - this.setState({ shouldAnimate: true }); - } - - componentWillUpdate(nextProps) { - if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) { - this.node.removeEventListener('wheel', this.handleWheel); - } - } - - componentDidUpdate(prevProps) { - if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - this.lastIndex = getIndex(this.context.router.history.location.pathname); - this.setState({ shouldAnimate: true }); - } - - componentWillUnmount () { - if (!this.props.singleColumn) { - this.node.removeEventListener('wheel', this.handleWheel); - } - } - - handleChildrenContentChange() { - if (!this.props.singleColumn) { - this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth); - } - } - - handleSwipe = (index) => { - this.pendingIndex = index; - - const nextLinkTranslationId = links[index].props['data-preview-title-id']; - const currentLinkSelector = '.tabs-bar__link.active'; - const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`; - - // HACK: Remove the active class from the current link and set it to the next one - // React-router does this for us, but too late, feeling laggy. - document.querySelector(currentLinkSelector).classList.remove('active'); - document.querySelector(nextLinkSelector).classList.add('active'); - } - - handleAnimationEnd = () => { - if (typeof this.pendingIndex === 'number') { - this.context.router.history.push(getLink(this.pendingIndex)); - this.pendingIndex = null; - } - } - - handleWheel = () => { - if (typeof this._interruptScrollAnimation !== 'function') { - return; - } - - this._interruptScrollAnimation(); - } - - setRef = (node) => { - this.node = node; - } - - renderView = (link, index) => { - const columnIndex = getIndex(this.context.router.history.location.pathname); - const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] }); - const icon = link.props['data-preview-icon']; - - const view = (index === columnIndex) ? - React.cloneElement(this.props.children) : - <ColumnLoading title={title} icon={icon} />; - - return ( - <div className='columns-area' key={index}> - {view} - </div> - ); - } - - renderLoading = columnId => () => { - return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />; - } - - renderError = (props) => { - return <BundleColumnError {...props} />; - } - - render () { - const { columns, children, singleColumn } = this.props; - const { shouldAnimate } = this.state; - - const columnIndex = getIndex(this.context.router.history.location.pathname); - this.pendingIndex = null; - - if (singleColumn) { - return columnIndex !== -1 ? ( - <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> - {links.map(this.renderView)} - </ReactSwipeableViews> - ) : <div className='columns-area'>{children}</div>; - } - - return ( - <div className='columns-area' ref={this.setRef}> - {columns.map(column => { - const params = column.get('params', null) === null ? null : column.get('params').toJS(); - - return ( - <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}> - {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn />} - </BundleContainer> - ); - })} - - {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js b/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js deleted file mode 100644 index 3d568aec3..000000000 --- a/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import Button from 'themes/glitch/components/button'; - -@injectIntl -export default class ConfirmationModal extends React.PureComponent { - - static propTypes = { - message: PropTypes.node.isRequired, - confirm: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - componentDidMount() { - this.button.focus(); - } - - handleClick = () => { - this.props.onClose(); - this.props.onConfirm(); - } - - handleCancel = () => { - this.props.onClose(); - } - - setRef = (c) => { - this.button = c; - } - - render () { - const { message, confirm } = this.props; - - return ( - <div className='modal-root__modal confirmation-modal'> - <div className='confirmation-modal__container'> - {message} - </div> - - <div className='confirmation-modal__action-bar'> - <Button onClick={this.handleCancel} className='confirmation-modal__cancel-button'> - <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /> - </Button> - <Button text={confirm} onClick={this.handleClick} ref={this.setRef} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/doodle_modal.js b/app/javascript/themes/glitch/features/ui/components/doodle_modal.js deleted file mode 100644 index 819656dbf..000000000 --- a/app/javascript/themes/glitch/features/ui/components/doodle_modal.js +++ /dev/null @@ -1,614 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Button from 'themes/glitch/components/button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Atrament from 'atrament'; // the doodling library -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { doodleSet, uploadCompose } from 'themes/glitch/actions/compose'; -import IconButton from 'themes/glitch/components/icon_button'; -import { debounce, mapValues } from 'lodash'; -import classNames from 'classnames'; - -// palette nicked from MyPaint, CC0 -const palette = [ - ['rgb( 0, 0, 0)', 'Black'], - ['rgb( 38, 38, 38)', 'Gray 15'], - ['rgb( 77, 77, 77)', 'Grey 30'], - ['rgb(128, 128, 128)', 'Grey 50'], - ['rgb(171, 171, 171)', 'Grey 67'], - ['rgb(217, 217, 217)', 'Grey 85'], - ['rgb(255, 255, 255)', 'White'], - ['rgb(128, 0, 0)', 'Maroon'], - ['rgb(209, 0, 0)', 'English-red'], - ['rgb(255, 54, 34)', 'Tomato'], - ['rgb(252, 60, 3)', 'Orange-red'], - ['rgb(255, 140, 105)', 'Salmon'], - ['rgb(252, 232, 32)', 'Cadium-yellow'], - ['rgb(243, 253, 37)', 'Lemon yellow'], - ['rgb(121, 5, 35)', 'Dark crimson'], - ['rgb(169, 32, 62)', 'Deep carmine'], - ['rgb(255, 140, 0)', 'Orange'], - ['rgb(255, 168, 18)', 'Dark tangerine'], - ['rgb(217, 144, 88)', 'Persian orange'], - ['rgb(194, 178, 128)', 'Sand'], - ['rgb(255, 229, 180)', 'Peach'], - ['rgb(100, 54, 46)', 'Bole'], - ['rgb(108, 41, 52)', 'Dark cordovan'], - ['rgb(163, 65, 44)', 'Chestnut'], - ['rgb(228, 136, 100)', 'Dark salmon'], - ['rgb(255, 195, 143)', 'Apricot'], - ['rgb(255, 219, 188)', 'Unbleached silk'], - ['rgb(242, 227, 198)', 'Straw'], - ['rgb( 53, 19, 13)', 'Bistre'], - ['rgb( 84, 42, 14)', 'Dark chocolate'], - ['rgb(102, 51, 43)', 'Burnt sienna'], - ['rgb(184, 66, 0)', 'Sienna'], - ['rgb(216, 153, 12)', 'Yellow ochre'], - ['rgb(210, 180, 140)', 'Tan'], - ['rgb(232, 204, 144)', 'Dark wheat'], - ['rgb( 0, 49, 83)', 'Prussian blue'], - ['rgb( 48, 69, 119)', 'Dark grey blue'], - ['rgb( 0, 71, 171)', 'Cobalt blue'], - ['rgb( 31, 117, 254)', 'Blue'], - ['rgb(120, 180, 255)', 'Bright french blue'], - ['rgb(171, 200, 255)', 'Bright steel blue'], - ['rgb(208, 231, 255)', 'Ice blue'], - ['rgb( 30, 51, 58)', 'Medium jungle green'], - ['rgb( 47, 79, 79)', 'Dark slate grey'], - ['rgb( 74, 104, 93)', 'Dark grullo green'], - ['rgb( 0, 128, 128)', 'Teal'], - ['rgb( 67, 170, 176)', 'Turquoise'], - ['rgb(109, 174, 199)', 'Cerulean frost'], - ['rgb(173, 217, 186)', 'Tiffany green'], - ['rgb( 22, 34, 29)', 'Gray-asparagus'], - ['rgb( 36, 48, 45)', 'Medium dark teal'], - ['rgb( 74, 104, 93)', 'Xanadu'], - ['rgb(119, 198, 121)', 'Mint'], - ['rgb(175, 205, 182)', 'Timberwolf'], - ['rgb(185, 245, 246)', 'Celeste'], - ['rgb(193, 255, 234)', 'Aquamarine'], - ['rgb( 29, 52, 35)', 'Cal Poly Pomona'], - ['rgb( 1, 68, 33)', 'Forest green'], - ['rgb( 42, 128, 0)', 'Napier green'], - ['rgb(128, 128, 0)', 'Olive'], - ['rgb( 65, 156, 105)', 'Sea green'], - ['rgb(189, 246, 29)', 'Green-yellow'], - ['rgb(231, 244, 134)', 'Bright chartreuse'], - ['rgb(138, 23, 137)', 'Purple'], - ['rgb( 78, 39, 138)', 'Violet'], - ['rgb(193, 75, 110)', 'Dark thulian pink'], - ['rgb(222, 49, 99)', 'Cerise'], - ['rgb(255, 20, 147)', 'Deep pink'], - ['rgb(255, 102, 204)', 'Rose pink'], - ['rgb(255, 203, 219)', 'Pink'], - ['rgb(255, 255, 255)', 'White'], - ['rgb(229, 17, 1)', 'RGB Red'], - ['rgb( 0, 255, 0)', 'RGB Green'], - ['rgb( 0, 0, 255)', 'RGB Blue'], - ['rgb( 0, 255, 255)', 'CMYK Cyan'], - ['rgb(255, 0, 255)', 'CMYK Magenta'], - ['rgb(255, 255, 0)', 'CMYK Yellow'], -]; - -// re-arrange to the right order for display -let palReordered = []; -for (let row = 0; row < 7; row++) { - for (let col = 0; col < 11; col++) { - palReordered.push(palette[col * 7 + row]); - } - palReordered.push(null); // null indicates a <br /> -} - -// Utility for converting base64 image to binary for upload -// https://stackoverflow.com/questions/35940290/how-to-convert-base64-string-to-javascript-file-object-like-as-from-file-input-f -function dataURLtoFile(dataurl, filename) { - let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], - bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); - while(n--){ - u8arr[n] = bstr.charCodeAt(n); - } - return new File([u8arr], filename, { type: mime }); -} - -const DOODLE_SIZES = { - normal: [500, 500, 'Square 500'], - tootbanner: [702, 330, 'Tootbanner'], - s640x480: [640, 480, '640×480 - 480p'], - s800x600: [800, 600, '800×600 - SVGA'], - s720x480: [720, 405, '720x405 - 16:9'], -}; - - -const mapStateToProps = state => ({ - options: state.getIn(['compose', 'doodle']), -}); - -const mapDispatchToProps = dispatch => ({ - /** Set options in the redux store */ - setOpt: (opts) => dispatch(doodleSet(opts)), - /** Submit doodle for upload */ - submit: (file) => dispatch(uploadCompose([file])), -}); - -/** - * Doodling dialog with drawing canvas - * - * Keyboard shortcuts: - * - Delete: Clear screen, fill with background color - * - Backspace, Ctrl+Z: Undo one step - * - Ctrl held while drawing: Use background color - * - Shift held while clicking screen: Use fill tool - * - * Palette: - * - Left mouse button: pick foreground - * - Ctrl + left mouse button: pick background - * - Right mouse button: pick background - */ -@connect(mapStateToProps, mapDispatchToProps) -export default class DoodleModal extends ImmutablePureComponent { - - static propTypes = { - options: ImmutablePropTypes.map, - onClose: PropTypes.func.isRequired, - setOpt: PropTypes.func.isRequired, - submit: PropTypes.func.isRequired, - }; - - //region Option getters/setters - - /** Foreground color */ - get fg () { - return this.props.options.get('fg'); - } - set fg (value) { - this.props.setOpt({ fg: value }); - } - - /** Background color */ - get bg () { - return this.props.options.get('bg'); - } - set bg (value) { - this.props.setOpt({ bg: value }); - } - - /** Swap Fg and Bg for drawing */ - get swapped () { - return this.props.options.get('swapped'); - } - set swapped (value) { - this.props.setOpt({ swapped: value }); - } - - /** Mode - 'draw' or 'fill' */ - get mode () { - return this.props.options.get('mode'); - } - set mode (value) { - this.props.setOpt({ mode: value }); - } - - /** Base line weight */ - get weight () { - return this.props.options.get('weight'); - } - set weight (value) { - this.props.setOpt({ weight: value }); - } - - /** Drawing opacity */ - get opacity () { - return this.props.options.get('opacity'); - } - set opacity (value) { - this.props.setOpt({ opacity: value }); - } - - /** Adaptive stroke - change width with speed */ - get adaptiveStroke () { - return this.props.options.get('adaptiveStroke'); - } - set adaptiveStroke (value) { - this.props.setOpt({ adaptiveStroke: value }); - } - - /** Smoothing (for mouse drawing) */ - get smoothing () { - return this.props.options.get('smoothing'); - } - set smoothing (value) { - this.props.setOpt({ smoothing: value }); - } - - /** Size preset */ - get size () { - return this.props.options.get('size'); - } - set size (value) { - this.props.setOpt({ size: value }); - } - - //endregion - - /** Key up handler */ - handleKeyUp = (e) => { - if (e.target.nodeName === 'INPUT') return; - - if (e.key === 'Delete') { - e.preventDefault(); - this.handleClearBtn(); - return; - } - - if (e.key === 'Backspace' || (e.key === 'z' && (e.ctrlKey || e.metaKey))) { - e.preventDefault(); - this.undo(); - } - - if (e.key === 'Control' || e.key === 'Meta') { - this.controlHeld = false; - this.swapped = false; - } - - if (e.key === 'Shift') { - this.shiftHeld = false; - this.mode = 'draw'; - } - }; - - /** Key down handler */ - handleKeyDown = (e) => { - if (e.key === 'Control' || e.key === 'Meta') { - this.controlHeld = true; - this.swapped = true; - } - - if (e.key === 'Shift') { - this.shiftHeld = true; - this.mode = 'fill'; - } - }; - - /** - * Component installed in the DOM, do some initial set-up - */ - componentDidMount () { - this.controlHeld = false; - this.shiftHeld = false; - this.swapped = false; - window.addEventListener('keyup', this.handleKeyUp, false); - window.addEventListener('keydown', this.handleKeyDown, false); - }; - - /** - * Tear component down - */ - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp, false); - window.removeEventListener('keydown', this.handleKeyDown, false); - if (this.sketcher) this.sketcher.destroy(); - } - - /** - * Set reference to the canvas element. - * This is called during component init - * - * @param elem - canvas element - */ - setCanvasRef = (elem) => { - this.canvas = elem; - if (elem) { - elem.addEventListener('dirty', () => { - this.saveUndo(); - this.sketcher._dirty = false; - }); - - elem.addEventListener('click', () => { - // sketcher bug - does not fire dirty on fill - if (this.mode === 'fill') { - this.saveUndo(); - } - }); - - // prevent context menu - elem.addEventListener('contextmenu', (e) => { - e.preventDefault(); - }); - - elem.addEventListener('mousedown', (e) => { - if (e.button === 2) { - this.swapped = true; - } - }); - - elem.addEventListener('mouseup', (e) => { - if (e.button === 2) { - this.swapped = this.controlHeld; - } - }); - - this.initSketcher(elem); - this.mode = 'draw'; // Reset mode - it's confusing if left at 'fill' - } - }; - - /** - * Set up the sketcher instance - * - * @param canvas - canvas element. Null if we're just resizing - */ - initSketcher (canvas = null) { - const sizepreset = DOODLE_SIZES[this.size]; - - if (this.sketcher) this.sketcher.destroy(); - this.sketcher = new Atrament(canvas || this.canvas, sizepreset[0], sizepreset[1]); - - if (canvas) { - this.ctx = this.sketcher.context; - this.updateSketcherSettings(); - } - - this.clearScreen(); - } - - /** - * Done button handler - */ - onDoneButton = () => { - const dataUrl = this.sketcher.toImage(); - const file = dataURLtoFile(dataUrl, 'doodle.png'); - this.props.submit(file); - this.props.onClose(); // close dialog - }; - - /** - * Cancel button handler - */ - onCancelButton = () => { - if (this.undos.length > 1 && !confirm('Discard doodle? All changes will be lost!')) { - return; - } - - this.props.onClose(); // close dialog - }; - - /** - * Update sketcher options based on state - */ - updateSketcherSettings () { - if (!this.sketcher) return; - - if (this.oldSize !== this.size) this.initSketcher(); - - this.sketcher.color = (this.swapped ? this.bg : this.fg); - this.sketcher.opacity = this.opacity; - this.sketcher.weight = this.weight; - this.sketcher.mode = this.mode; - this.sketcher.smoothing = this.smoothing; - this.sketcher.adaptiveStroke = this.adaptiveStroke; - - this.oldSize = this.size; - } - - /** - * Fill screen with background color - */ - clearScreen = () => { - this.ctx.fillStyle = this.bg; - this.ctx.fillRect(-1, -1, this.canvas.width+2, this.canvas.height+2); - this.undos = []; - - this.doSaveUndo(); - }; - - /** - * Undo one step - */ - undo = () => { - if (this.undos.length > 1) { - this.undos.pop(); - const buf = this.undos.pop(); - - this.sketcher.clear(); - this.ctx.putImageData(buf, 0, 0); - this.doSaveUndo(); - } - }; - - /** - * Save canvas content into the undo buffer immediately - */ - doSaveUndo = () => { - this.undos.push(this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)); - }; - - /** - * Called on each canvas change. - * Saves canvas content to the undo buffer after some period of inactivity. - */ - saveUndo = debounce(() => { - this.doSaveUndo(); - }, 100); - - /** - * Palette left click. - * Selects Fg color (or Bg, if Control/Meta is held) - * - * @param e - event - */ - onPaletteClick = (e) => { - const c = e.target.dataset.color; - - if (this.controlHeld) { - this.bg = c; - } else { - this.fg = c; - } - - e.target.blur(); - e.preventDefault(); - }; - - /** - * Palette right click. - * Selects Bg color - * - * @param e - event - */ - onPaletteRClick = (e) => { - this.bg = e.target.dataset.color; - e.target.blur(); - e.preventDefault(); - }; - - /** - * Handle click on the Draw mode button - * - * @param e - event - */ - setModeDraw = (e) => { - this.mode = 'draw'; - e.target.blur(); - }; - - /** - * Handle click on the Fill mode button - * - * @param e - event - */ - setModeFill = (e) => { - this.mode = 'fill'; - e.target.blur(); - }; - - /** - * Handle click on Smooth checkbox - * - * @param e - event - */ - tglSmooth = (e) => { - this.smoothing = !this.smoothing; - e.target.blur(); - }; - - /** - * Handle click on Adaptive checkbox - * - * @param e - event - */ - tglAdaptive = (e) => { - this.adaptiveStroke = !this.adaptiveStroke; - e.target.blur(); - }; - - /** - * Handle change of the Weight input field - * - * @param e - event - */ - setWeight = (e) => { - this.weight = +e.target.value || 1; - }; - - /** - * Set size - clalback from the select box - * - * @param e - event - */ - changeSize = (e) => { - let newSize = e.target.value; - if (newSize === this.oldSize) return; - - if (this.undos.length > 1 && !confirm('Change size? This will erase your drawing!')) { - return; - } - - this.size = newSize; - }; - - handleClearBtn = () => { - if (this.undos.length > 1 && !confirm('Clear screen? This will erase your drawing!')) { - return; - } - - this.clearScreen(); - }; - - /** - * Render the component - */ - render () { - this.updateSketcherSettings(); - - return ( - <div className='modal-root__modal doodle-modal'> - <div className='doodle-modal__container'> - <canvas ref={this.setCanvasRef} /> - </div> - - <div className='doodle-modal__action-bar'> - <div className='doodle-toolbar'> - <Button text='Done' onClick={this.onDoneButton} /> - <Button text='Cancel' onClick={this.onCancelButton} /> - </div> - <div className='filler' /> - <div className='doodle-toolbar with-inputs'> - <div> - <label htmlFor='dd_smoothing'>Smoothing</label> - <span className='val'> - <input type='checkbox' id='dd_smoothing' onChange={this.tglSmooth} checked={this.smoothing} /> - </span> - </div> - <div> - <label htmlFor='dd_adaptive'>Adaptive</label> - <span className='val'> - <input type='checkbox' id='dd_adaptive' onChange={this.tglAdaptive} checked={this.adaptiveStroke} /> - </span> - </div> - <div> - <label htmlFor='dd_weight'>Weight</label> - <span className='val'> - <input type='number' min={1} id='dd_weight' value={this.weight} onChange={this.setWeight} /> - </span> - </div> - <div> - <select aria-label='Canvas size' onInput={this.changeSize} defaultValue={this.size}> - { Object.values(mapValues(DOODLE_SIZES, (val, k) => - <option key={k} value={k}>{val[2]}</option> - )) } - </select> - </div> - </div> - <div className='doodle-toolbar'> - <IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted /> - <IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted /> - <IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted /> - <IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted /> - </div> - <div className='doodle-palette'> - { - palReordered.map((c, i) => - c === null ? - <br key={i} /> : - <button - key={i} - style={{ backgroundColor: c[0] }} - onClick={this.onPaletteClick} - onContextMenu={this.onPaletteRClick} - data-color={c[0]} - title={c[1]} - className={classNames({ - 'foreground': this.fg === c[0], - 'background': this.bg === c[0], - })} - /> - ) - } - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/drawer_loading.js b/app/javascript/themes/glitch/features/ui/components/drawer_loading.js deleted file mode 100644 index 08b0d2347..000000000 --- a/app/javascript/themes/glitch/features/ui/components/drawer_loading.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -const DrawerLoading = () => ( - <div className='drawer'> - <div className='drawer__pager'> - <div className='drawer__inner' /> - </div> - </div> -); - -export default DrawerLoading; diff --git a/app/javascript/themes/glitch/features/ui/components/embed_modal.js b/app/javascript/themes/glitch/features/ui/components/embed_modal.js deleted file mode 100644 index 1afffb51b..000000000 --- a/app/javascript/themes/glitch/features/ui/components/embed_modal.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import axios from 'axios'; - -@injectIntl -export default class EmbedModal extends ImmutablePureComponent { - - static propTypes = { - url: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - } - - state = { - loading: false, - oembed: null, - }; - - componentDidMount () { - const { url } = this.props; - - this.setState({ loading: true }); - - axios.post('/api/web/embed', { url }).then(res => { - this.setState({ loading: false, oembed: res.data }); - - const iframeDocument = this.iframe.contentWindow.document; - - iframeDocument.open(); - iframeDocument.write(res.data.html); - iframeDocument.close(); - - iframeDocument.body.style.margin = 0; - this.iframe.width = iframeDocument.body.scrollWidth; - this.iframe.height = iframeDocument.body.scrollHeight; - }); - } - - setIframeRef = c => { - this.iframe = c; - } - - handleTextareaClick = (e) => { - e.target.select(); - } - - render () { - const { oembed } = this.state; - - return ( - <div className='modal-root__modal embed-modal'> - <h4><FormattedMessage id='status.embed' defaultMessage='Embed' /></h4> - - <div className='embed-modal__container'> - <p className='hint'> - <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' /> - </p> - - <input - type='text' - className='embed-modal__html' - readOnly - value={oembed && oembed.html || ''} - onClick={this.handleTextareaClick} - /> - - <p className='hint'> - <FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' /> - </p> - - <iframe - className='embed-modal__iframe' - frameBorder='0' - ref={this.setIframeRef} - title='preview' - /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/image_loader.js b/app/javascript/themes/glitch/features/ui/components/image_loader.js deleted file mode 100644 index aad594380..000000000 --- a/app/javascript/themes/glitch/features/ui/components/image_loader.js +++ /dev/null @@ -1,152 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class ImageLoader extends React.PureComponent { - - static propTypes = { - alt: PropTypes.string, - src: PropTypes.string.isRequired, - previewSrc: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - } - - static defaultProps = { - alt: '', - width: null, - height: null, - }; - - state = { - loading: true, - error: false, - } - - removers = []; - - get canvasContext() { - if (!this.canvas) { - return null; - } - this._canvasContext = this._canvasContext || this.canvas.getContext('2d'); - return this._canvasContext; - } - - componentDidMount () { - this.loadImage(this.props); - } - - componentWillReceiveProps (nextProps) { - if (this.props.src !== nextProps.src) { - this.loadImage(nextProps); - } - } - - loadImage (props) { - this.removeEventListeners(); - this.setState({ loading: true, error: false }); - Promise.all([ - this.loadPreviewCanvas(props), - this.hasSize() && this.loadOriginalImage(props), - ].filter(Boolean)) - .then(() => { - this.setState({ loading: false, error: false }); - this.clearPreviewCanvas(); - }) - .catch(() => this.setState({ loading: false, error: true })); - } - - loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => { - const image = new Image(); - const removeEventListeners = () => { - image.removeEventListener('error', handleError); - image.removeEventListener('load', handleLoad); - }; - const handleError = () => { - removeEventListeners(); - reject(); - }; - const handleLoad = () => { - removeEventListeners(); - this.canvasContext.drawImage(image, 0, 0, width, height); - resolve(); - }; - image.addEventListener('error', handleError); - image.addEventListener('load', handleLoad); - image.src = previewSrc; - this.removers.push(removeEventListeners); - }) - - clearPreviewCanvas () { - const { width, height } = this.canvas; - this.canvasContext.clearRect(0, 0, width, height); - } - - loadOriginalImage = ({ src }) => new Promise((resolve, reject) => { - const image = new Image(); - const removeEventListeners = () => { - image.removeEventListener('error', handleError); - image.removeEventListener('load', handleLoad); - }; - const handleError = () => { - removeEventListeners(); - reject(); - }; - const handleLoad = () => { - removeEventListeners(); - resolve(); - }; - image.addEventListener('error', handleError); - image.addEventListener('load', handleLoad); - image.src = src; - this.removers.push(removeEventListeners); - }); - - removeEventListeners () { - this.removers.forEach(listeners => listeners()); - this.removers = []; - } - - hasSize () { - const { width, height } = this.props; - return typeof width === 'number' && typeof height === 'number'; - } - - setCanvasRef = c => { - this.canvas = c; - } - - render () { - const { alt, src, width, height } = this.props; - const { loading } = this.state; - - const className = classNames('image-loader', { - 'image-loader--loading': loading, - 'image-loader--amorphous': !this.hasSize(), - }); - - return ( - <div className={className}> - <canvas - className='image-loader__preview-canvas' - width={width} - height={height} - ref={this.setCanvasRef} - style={{ opacity: loading ? 1 : 0 }} - /> - - {!loading && ( - <img - alt={alt} - className='image-loader__img' - src={src} - width={width} - height={height} - /> - )} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/media_modal.js b/app/javascript/themes/glitch/features/ui/components/media_modal.js deleted file mode 100644 index 1dad972b2..000000000 --- a/app/javascript/themes/glitch/features/ui/components/media_modal.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import ReactSwipeableViews from 'react-swipeable-views'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ExtendedVideoPlayer from 'themes/glitch/components/extended_video_player'; -import { defineMessages, injectIntl } from 'react-intl'; -import IconButton from 'themes/glitch/components/icon_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImageLoader from './image_loader'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, - previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, - next: { id: 'lightbox.next', defaultMessage: 'Next' }, -}); - -@injectIntl -export default class MediaModal extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.list.isRequired, - index: PropTypes.number.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - index: null, - }; - - handleSwipe = (index) => { - this.setState({ index: index % this.props.media.size }); - } - - handleNextClick = () => { - this.setState({ index: (this.getIndex() + 1) % this.props.media.size }); - } - - handlePrevClick = () => { - this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size }); - } - - handleChangeIndex = (e) => { - const index = Number(e.currentTarget.getAttribute('data-index')); - this.setState({ index: index % this.props.media.size }); - } - - handleKeyUp = (e) => { - switch(e.key) { - case 'ArrowLeft': - this.handlePrevClick(); - break; - case 'ArrowRight': - this.handleNextClick(); - break; - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - getIndex () { - return this.state.index !== null ? this.state.index : this.props.index; - } - - render () { - const { media, intl, onClose } = this.props; - - const index = this.getIndex(); - let pagination = []; - - const leftNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>; - const rightNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>; - - if (media.size > 1) { - pagination = media.map((item, i) => { - const classes = ['media-modal__button']; - if (i === index) { - classes.push('media-modal__button--active'); - } - return (<li className='media-modal__page-dot' key={i}><button tabIndex='0' className={classes.join(' ')} onClick={this.handleChangeIndex} data-index={i}>{i + 1}</button></li>); - }); - } - - const content = media.map((image) => { - const width = image.getIn(['meta', 'original', 'width']) || null; - const height = image.getIn(['meta', 'original', 'height']) || null; - - if (image.get('type') === 'image') { - return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} alt={image.get('description')} key={image.get('preview_url')} />; - } else if (image.get('type') === 'gifv') { - return <ExtendedVideoPlayer src={image.get('url')} muted controls={false} width={width} height={height} key={image.get('preview_url')} alt={image.get('description')} />; - } - - return null; - }).toArray(); - - const containerStyle = { - alignItems: 'center', // center vertically - }; - - return ( - <div className='modal-root__modal media-modal'> - {leftNav} - - <div className='media-modal__content'> - <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> - <ReactSwipeableViews containerStyle={containerStyle} onChangeIndex={this.handleSwipe} index={index}> - {content} - </ReactSwipeableViews> - </div> - <ul className='media-modal__pagination'> - {pagination} - </ul> - - {rightNav} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/modal_loading.js b/app/javascript/themes/glitch/features/ui/components/modal_loading.js deleted file mode 100644 index e14d20fbb..000000000 --- a/app/javascript/themes/glitch/features/ui/components/modal_loading.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; - -// Keep the markup in sync with <BundleModalError /> -// (make sure they have the same dimensions) -const ModalLoading = () => ( - <div className='modal-root__modal error-modal'> - <div className='error-modal__body'> - <LoadingIndicator /> - </div> - <div className='error-modal__footer'> - <div> - <button className='error-modal__nav onboarding-modal__skip' /> - </div> - </div> - </div> -); - -export default ModalLoading; diff --git a/app/javascript/themes/glitch/features/ui/components/modal_root.js b/app/javascript/themes/glitch/features/ui/components/modal_root.js deleted file mode 100644 index fbe794170..000000000 --- a/app/javascript/themes/glitch/features/ui/components/modal_root.js +++ /dev/null @@ -1,131 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import BundleContainer from '../containers/bundle_container'; -import BundleModalError from './bundle_modal_error'; -import ModalLoading from './modal_loading'; -import ActionsModal from './actions_modal'; -import MediaModal from './media_modal'; -import VideoModal from './video_modal'; -import BoostModal from './boost_modal'; -import DoodleModal from './doodle_modal'; -import ConfirmationModal from './confirmation_modal'; -import { - OnboardingModal, - MuteModal, - ReportModal, - SettingsModal, - EmbedModal, -} from 'themes/glitch/util/async-components'; - -const MODAL_COMPONENTS = { - 'MEDIA': () => Promise.resolve({ default: MediaModal }), - 'ONBOARDING': OnboardingModal, - 'VIDEO': () => Promise.resolve({ default: VideoModal }), - 'BOOST': () => Promise.resolve({ default: BoostModal }), - 'DOODLE': () => Promise.resolve({ default: DoodleModal }), - 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), - 'MUTE': MuteModal, - 'REPORT': ReportModal, - 'SETTINGS': SettingsModal, - 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), - 'EMBED': EmbedModal, -}; - -export default class ModalRoot extends React.PureComponent { - - static propTypes = { - type: PropTypes.string, - props: PropTypes.object, - onClose: PropTypes.func.isRequired, - }; - - state = { - revealed: false, - }; - - handleKeyUp = (e) => { - if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) - && !!this.props.type && !this.props.props.noEsc) { - this.props.onClose(); - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillReceiveProps (nextProps) { - if (!!nextProps.type && !this.props.type) { - this.activeElement = document.activeElement; - - this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); - } else if (!nextProps.type) { - this.setState({ revealed: false }); - } - } - - componentDidUpdate (prevProps) { - if (!this.props.type && !!prevProps.type) { - this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); - this.activeElement.focus(); - this.activeElement = null; - } - if (this.props.type) { - requestAnimationFrame(() => { - this.setState({ revealed: true }); - }); - } - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - getSiblings = () => { - return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); - } - - setRef = ref => { - this.node = ref; - } - - renderLoading = modalId => () => { - return ['MEDIA', 'VIDEO', 'BOOST', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; - } - - renderError = (props) => { - const { onClose } = this.props; - - return <BundleModalError {...props} onClose={onClose} />; - } - - render () { - const { type, props, onClose } = this.props; - const { revealed } = this.state; - const visible = !!type; - - if (!visible) { - return ( - <div className='modal-root' ref={this.setRef} style={{ opacity: 0 }} /> - ); - } - - return ( - <div className='modal-root' ref={this.setRef} style={{ opacity: revealed ? 1 : 0 }}> - <div style={{ pointerEvents: visible ? 'auto' : 'none' }}> - <div role='presentation' className='modal-root__overlay' onClick={onClose} /> - <div role='dialog' className='modal-root__container'> - { - visible ? - (<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> - {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} - </BundleContainer>) : - null - } - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/mute_modal.js b/app/javascript/themes/glitch/features/ui/components/mute_modal.js deleted file mode 100644 index ffccdc84d..000000000 --- a/app/javascript/themes/glitch/features/ui/components/mute_modal.js +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import Toggle from 'react-toggle'; -import Button from 'themes/glitch/components/button'; -import { closeModal } from 'themes/glitch/actions/modal'; -import { muteAccount } from 'themes/glitch/actions/accounts'; -import { toggleHideNotifications } from 'themes/glitch/actions/mutes'; - - -const mapStateToProps = state => { - return { - isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), - account: state.getIn(['mutes', 'new', 'account']), - notifications: state.getIn(['mutes', 'new', 'notifications']), - }; -}; - -const mapDispatchToProps = dispatch => { - return { - onConfirm(account, notifications) { - dispatch(muteAccount(account.get('id'), notifications)); - }, - - onClose() { - dispatch(closeModal()); - }, - - onToggleNotifications() { - dispatch(toggleHideNotifications()); - }, - }; -}; - -@connect(mapStateToProps, mapDispatchToProps) -@injectIntl -export default class MuteModal extends React.PureComponent { - - static propTypes = { - isSubmitting: PropTypes.bool.isRequired, - account: PropTypes.object.isRequired, - notifications: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, - onToggleNotifications: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - componentDidMount() { - this.button.focus(); - } - - handleClick = () => { - this.props.onClose(); - this.props.onConfirm(this.props.account, this.props.notifications); - } - - handleCancel = () => { - this.props.onClose(); - } - - setRef = (c) => { - this.button = c; - } - - toggleNotifications = () => { - this.props.onToggleNotifications(); - } - - render () { - const { account, notifications } = this.props; - - return ( - <div className='modal-root__modal mute-modal'> - <div className='mute-modal__container'> - <p> - <FormattedMessage - id='confirmations.mute.message' - defaultMessage='Are you sure you want to mute {name}?' - values={{ name: <strong>@{account.get('acct')}</strong> }} - /> - </p> - <div> - <label htmlFor='mute-modal__hide-notifications-checkbox'> - <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' /> - {' '} - <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} /> - </label> - </div> - </div> - - <div className='mute-modal__action-bar'> - <Button onClick={this.handleCancel} className='mute-modal__cancel-button'> - <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /> - </Button> - <Button onClick={this.handleClick} ref={this.setRef}> - <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' /> - </Button> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js b/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js deleted file mode 100644 index 58875262e..000000000 --- a/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js +++ /dev/null @@ -1,323 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ReactSwipeableViews from 'react-swipeable-views'; -import classNames from 'classnames'; -import Permalink from 'themes/glitch/components/permalink'; -import ComposeForm from 'themes/glitch/features/compose/components/compose_form'; -import Search from 'themes/glitch/features/compose/components/search'; -import NavigationBar from 'themes/glitch/features/compose/components/navigation_bar'; -import ColumnHeader from './column_header'; -import { - List as ImmutableList, - Map as ImmutableMap, -} from 'immutable'; -import { me } from 'themes/glitch/util/initial_state'; - -const noop = () => { }; - -const messages = defineMessages({ - home_title: { id: 'column.home', defaultMessage: 'Home' }, - notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' }, - local_title: { id: 'column.community', defaultMessage: 'Local timeline' }, - federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' }, -}); - -const PageOne = ({ acct, domain }) => ( - <div className='onboarding-modal__page onboarding-modal__page-one'> - <div style={{ flex: '0 0 auto' }}> - <div className='onboarding-modal__page-one__elephant-friend' /> - </div> - - <div> - <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to {domain}!' values={{ domain }} /></h1> - <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='{domain} is an "instance" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' values={{ domain }} /></p> - <p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>@{acct}@{domain}</strong> }} /></p> - </div> - </div> -); - -PageOne.propTypes = { - acct: PropTypes.string.isRequired, - domain: PropTypes.string.isRequired, -}; - -const PageTwo = ({ myAccount }) => ( - <div className='onboarding-modal__page onboarding-modal__page-two'> - <div className='figure non-interactive'> - <div className='pseudo-drawer'> - <NavigationBar onClose={noop} account={myAccount} /> - </div> - <ComposeForm - text='Awoo! #introductions' - suggestions={ImmutableList()} - mentionedDomains={[]} - spoiler={false} - onChange={noop} - onSubmit={noop} - onPaste={noop} - onPickEmoji={noop} - onChangeSpoilerText={noop} - onClearSuggestions={noop} - onFetchSuggestions={noop} - onSuggestionSelected={noop} - onPrivacyChange={noop} - showSearch - settings={ImmutableMap.of('side_arm', 'none')} - /> - </div> - - <p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p> - </div> -); - -PageTwo.propTypes = { - myAccount: ImmutablePropTypes.map.isRequired, -}; - -const PageThree = ({ myAccount }) => ( - <div className='onboarding-modal__page onboarding-modal__page-three'> - <div className='figure non-interactive'> - <Search - value='' - onChange={noop} - onSubmit={noop} - onClear={noop} - onShow={noop} - /> - - <div className='pseudo-drawer'> - <NavigationBar onClose={noop} account={myAccount} /> - </div> - </div> - - <p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }} /></p> - <p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p> - </div> -); - -PageThree.propTypes = { - myAccount: ImmutablePropTypes.map.isRequired, -}; - -const PageFour = ({ domain, intl }) => ( - <div className='onboarding-modal__page onboarding-modal__page-four'> - <div className='onboarding-modal__page-four__columns'> - <div className='row'> - <div> - <div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div> - <p><FormattedMessage id='onboarding.page_four.home' defaultMessage='The home timeline shows posts from people you follow.' /></p> - </div> - - <div> - <div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div> - <p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='The notifications column shows when someone interacts with you.' /></p> - </div> - </div> - - <div className='row'> - <div> - <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div> - </div> - - <div> - <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div> - </div> - </div> - - <p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.' values={{ domain }} /></p> - </div> - </div> -); - -PageFour.propTypes = { - domain: PropTypes.string.isRequired, - intl: PropTypes.object.isRequired, -}; - -const PageSix = ({ admin, domain }) => { - let adminSection = ''; - - if (admin) { - adminSection = ( - <p> - <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} /> - <br /> - <FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage="Please read {domain}'s {guidelines}!" values={{ domain, guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }} /> - </p> - ); - } - - return ( - <div className='onboarding-modal__page onboarding-modal__page-six'> - <h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1> - {adminSection} - <p><FormattedMessage id='onboarding.page_six.github' defaultMessage='{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ domain, fork: <a href='https://en.wikipedia.org/wiki/Fork_(software_development)' target='_blank' rel='noopener'>fork</a>, Mastodon: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>Mastodon</a>, github: <a href='https://github.com/glitch-soc/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p> - <p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ domain, apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /></a> }} /></p> - <p><em><FormattedMessage id='onboarding.page_six.appetoot' defaultMessage='Bon Appetoot!' /></em></p> - </div> - ); -}; - -PageSix.propTypes = { - admin: ImmutablePropTypes.map, - domain: PropTypes.string.isRequired, -}; - -const mapStateToProps = state => ({ - myAccount: state.getIn(['accounts', me]), - admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]), - domain: state.getIn(['meta', 'domain']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class OnboardingModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - myAccount: ImmutablePropTypes.map.isRequired, - domain: PropTypes.string.isRequired, - admin: ImmutablePropTypes.map, - }; - - state = { - currentIndex: 0, - }; - - componentWillMount() { - const { myAccount, admin, domain, intl } = this.props; - this.pages = [ - <PageOne acct={myAccount.get('acct')} domain={domain} />, - <PageTwo myAccount={myAccount} />, - <PageThree myAccount={myAccount} />, - <PageFour domain={domain} intl={intl} />, - <PageSix admin={admin} domain={domain} />, - ]; - }; - - componentDidMount() { - window.addEventListener('keyup', this.handleKeyUp); - } - - componentWillUnmount() { - window.addEventListener('keyup', this.handleKeyUp); - } - - handleSkip = (e) => { - e.preventDefault(); - this.props.onClose(); - } - - handleDot = (e) => { - const i = Number(e.currentTarget.getAttribute('data-index')); - e.preventDefault(); - this.setState({ currentIndex: i }); - } - - handlePrev = () => { - this.setState(({ currentIndex }) => ({ - currentIndex: Math.max(0, currentIndex - 1), - })); - } - - handleNext = () => { - const { pages } = this; - this.setState(({ currentIndex }) => ({ - currentIndex: Math.min(currentIndex + 1, pages.length - 1), - })); - } - - handleSwipe = (index) => { - this.setState({ currentIndex: index }); - } - - handleKeyUp = ({ key }) => { - switch (key) { - case 'ArrowLeft': - this.handlePrev(); - break; - case 'ArrowRight': - this.handleNext(); - break; - } - } - - handleClose = () => { - this.props.onClose(); - } - - render () { - const { pages } = this; - const { currentIndex } = this.state; - const hasMore = currentIndex < pages.length - 1; - - const nextOrDoneBtn = hasMore ? ( - <button - onClick={this.handleNext} - className='onboarding-modal__nav onboarding-modal__next' - > - <FormattedMessage id='onboarding.next' defaultMessage='Next' /> - </button> - ) : ( - <button - onClick={this.handleClose} - className='onboarding-modal__nav onboarding-modal__done' - > - <FormattedMessage id='onboarding.done' defaultMessage='Done' /> - </button> - ); - - return ( - <div className='modal-root__modal onboarding-modal'> - <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='onboarding-modal__pager'> - {pages.map((page, i) => { - const className = classNames('onboarding-modal__page__wrapper', { - 'onboarding-modal__page__wrapper--active': i === currentIndex, - }); - return ( - <div key={i} className={className}>{page}</div> - ); - })} - </ReactSwipeableViews> - - <div className='onboarding-modal__paginator'> - <div> - <button - onClick={this.handleSkip} - className='onboarding-modal__nav onboarding-modal__skip' - > - <FormattedMessage id='onboarding.skip' defaultMessage='Skip' /> - </button> - </div> - - <div className='onboarding-modal__dots'> - {pages.map((_, i) => { - const className = classNames('onboarding-modal__dot', { - active: i === currentIndex, - }); - return ( - <div - key={`dot-${i}`} - role='button' - tabIndex='0' - data-index={i} - onClick={this.handleDot} - className={className} - /> - ); - })} - </div> - - <div> - {nextOrDoneBtn} - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/report_modal.js b/app/javascript/themes/glitch/features/ui/components/report_modal.js deleted file mode 100644 index e6153948e..000000000 --- a/app/javascript/themes/glitch/features/ui/components/report_modal.js +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { changeReportComment, submitReport } from 'themes/glitch/actions/reports'; -import { refreshAccountTimeline } from 'themes/glitch/actions/timelines'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { makeGetAccount } from 'themes/glitch/selectors'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import StatusCheckBox from 'themes/glitch/features/report/containers/status_check_box_container'; -import { OrderedSet } from 'immutable'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Button from 'themes/glitch/components/button'; - -const messages = defineMessages({ - placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, - submit: { id: 'report.submit', defaultMessage: 'Submit' }, -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = state => { - const accountId = state.getIn(['reports', 'new', 'account_id']); - - return { - isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), - account: getAccount(state, accountId), - comment: state.getIn(['reports', 'new', 'comment']), - statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), - }; - }; - - return mapStateToProps; -}; - -@connect(makeMapStateToProps) -@injectIntl -export default class ReportModal extends ImmutablePureComponent { - - static propTypes = { - isSubmitting: PropTypes.bool, - account: ImmutablePropTypes.map, - statusIds: ImmutablePropTypes.orderedSet.isRequired, - comment: PropTypes.string.isRequired, - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleCommentChange = (e) => { - this.props.dispatch(changeReportComment(e.target.value)); - } - - handleSubmit = () => { - this.props.dispatch(submitReport()); - } - - componentDidMount () { - this.props.dispatch(refreshAccountTimeline(this.props.account.get('id'))); - } - - componentWillReceiveProps (nextProps) { - if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(refreshAccountTimeline(nextProps.account.get('id'))); - } - } - - render () { - const { account, comment, intl, statusIds, isSubmitting } = this.props; - - if (!account) { - return null; - } - - return ( - <div className='modal-root__modal report-modal'> - <div className='report-modal__target'> - <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} /> - </div> - - <div className='report-modal__container'> - <div className='report-modal__statuses'> - <div> - {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} - </div> - </div> - - <div className='report-modal__comment'> - <textarea - className='setting-text light' - placeholder={intl.formatMessage(messages.placeholder)} - value={comment} - onChange={this.handleCommentChange} - disabled={isSubmitting} - /> - </div> - </div> - - <div className='report-modal__action-bar'> - <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/tabs_bar.js b/app/javascript/themes/glitch/features/ui/components/tabs_bar.js deleted file mode 100644 index ef5deae99..000000000 --- a/app/javascript/themes/glitch/features/ui/components/tabs_bar.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { NavLink } from 'react-router-dom'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { debounce } from 'lodash'; -import { isUserTouching } from 'themes/glitch/util/is_mobile'; - -export const links = [ - <NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>, - <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, - <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, - - <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, - <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, - - <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>, -]; - -export function getIndex (path) { - return links.findIndex(link => link.props.to === path); -} - -export function getLink (index) { - return links[index].props.to; -} - -@injectIntl -export default class TabsBar extends React.Component { - - static contextTypes = { - router: PropTypes.object.isRequired, - } - - static propTypes = { - intl: PropTypes.object.isRequired, - } - - setRef = ref => { - this.node = ref; - } - - handleClick = (e) => { - // Only apply optimization for touch devices, which we assume are slower - // We thus avoid the 250ms delay for non-touch devices and the lag for touch devices - if (isUserTouching()) { - e.preventDefault(); - e.persist(); - - requestAnimationFrame(() => { - const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link')); - const currentTab = tabs.find(tab => tab.classList.contains('active')); - 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'); - } - - const listener = debounce(() => { - nextTab.removeEventListener('transitionend', listener); - this.context.router.history.push(to); - }, 50); - - nextTab.addEventListener('transitionend', listener); - nextTab.classList.add('active'); - } - }); - } - - } - - render () { - const { intl: { formatMessage } } = this.props; - - return ( - <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> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/upload_area.js b/app/javascript/themes/glitch/features/ui/components/upload_area.js deleted file mode 100644 index 72a450215..000000000 --- a/app/javascript/themes/glitch/features/ui/components/upload_area.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { FormattedMessage } from 'react-intl'; - -export default class UploadArea extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - onClose: PropTypes.func, - }; - - handleKeyUp = (e) => { - const keyCode = e.keyCode; - if (this.props.active) { - switch(keyCode) { - case 27: - e.preventDefault(); - e.stopPropagation(); - this.props.onClose(); - break; - } - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - render () { - const { active } = this.props; - - return ( - <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}> - {({ backgroundOpacity, backgroundScale }) => - <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}> - <div className='upload-area__drop'> - <div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} /> - <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div> - </div> - </div> - } - </Motion> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/video_modal.js b/app/javascript/themes/glitch/features/ui/components/video_modal.js deleted file mode 100644 index 91168c790..000000000 --- a/app/javascript/themes/glitch/features/ui/components/video_modal.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Video from 'themes/glitch/features/video'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class VideoModal extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - time: PropTypes.number, - onClose: PropTypes.func.isRequired, - }; - - render () { - const { media, time, onClose } = this.props; - - return ( - <div className='modal-root__modal media-modal'> - <div> - <Video - preview={media.get('preview_url')} - src={media.get('url')} - startTime={time} - onCloseVideo={onClose} - description={media.get('description')} - /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/containers/bundle_container.js b/app/javascript/themes/glitch/features/ui/containers/bundle_container.js deleted file mode 100644 index e6f9afcf7..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/bundle_container.js +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux'; - -import Bundle from '../components/bundle'; - -import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from 'themes/glitch/actions/bundles'; - -const mapDispatchToProps = dispatch => ({ - onFetch () { - dispatch(fetchBundleRequest()); - }, - onFetchSuccess () { - dispatch(fetchBundleSuccess()); - }, - onFetchFail (error) { - dispatch(fetchBundleFail(error)); - }, -}); - -export default connect(null, mapDispatchToProps)(Bundle); diff --git a/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js b/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js deleted file mode 100644 index 95f95618b..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnsArea from '../components/columns_area'; - -const mapStateToProps = state => ({ - columns: state.getIn(['settings', 'columns']), -}); - -export default connect(mapStateToProps, null, null, { withRef: true })(ColumnsArea); diff --git a/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js b/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js deleted file mode 100644 index 4bb90fb68..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import LoadingBar from 'react-redux-loading-bar'; - -const mapStateToProps = (state) => ({ - loading: state.get('loadingBar'), -}); - -export default connect(mapStateToProps)(LoadingBar.WrappedComponent); diff --git a/app/javascript/themes/glitch/features/ui/containers/modal_container.js b/app/javascript/themes/glitch/features/ui/containers/modal_container.js deleted file mode 100644 index c26f19886..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/modal_container.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; -import { closeModal } from 'themes/glitch/actions/modal'; -import ModalRoot from '../components/modal_root'; - -const mapStateToProps = state => ({ - type: state.get('modal').modalType, - props: state.get('modal').modalProps, -}); - -const mapDispatchToProps = dispatch => ({ - onClose () { - dispatch(closeModal()); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot); diff --git a/app/javascript/themes/glitch/features/ui/containers/notifications_container.js b/app/javascript/themes/glitch/features/ui/containers/notifications_container.js deleted file mode 100644 index 5bd4017f5..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/notifications_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from 'react-redux'; -import { NotificationStack } from 'react-notification'; -import { dismissAlert } from 'themes/glitch/actions/alerts'; -import { getAlerts } from 'themes/glitch/selectors'; - -const mapStateToProps = state => ({ - notifications: getAlerts(state), -}); - -const mapDispatchToProps = (dispatch) => { - return { - onDismiss: alert => { - dispatch(dismissAlert(alert)); - }, - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(NotificationStack); diff --git a/app/javascript/themes/glitch/features/ui/containers/status_list_container.js b/app/javascript/themes/glitch/features/ui/containers/status_list_container.js deleted file mode 100644 index 807c82e16..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/status_list_container.js +++ /dev/null @@ -1,73 +0,0 @@ -import { connect } from 'react-redux'; -import StatusList from 'themes/glitch/components/status_list'; -import { scrollTopTimeline } from 'themes/glitch/actions/timelines'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import { createSelector } from 'reselect'; -import { debounce } from 'lodash'; -import { me } from 'themes/glitch/util/initial_state'; - -const makeGetStatusIds = () => createSelector([ - (state, { type }) => state.getIn(['settings', type], ImmutableMap()), - (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), - (state) => state.get('statuses'), -], (columnSettings, statusIds, statuses) => { - const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim(); - let regex = null; - - try { - regex = rawRegex && new RegExp(rawRegex, 'i'); - } catch (e) { - // Bad regex, don't affect filters - } - - return statusIds.filter(id => { - const statusForId = statuses.get(id); - let showStatus = true; - - if (columnSettings.getIn(['shows', 'reblog']) === false) { - showStatus = showStatus && statusForId.get('reblog') === null; - } - - if (columnSettings.getIn(['shows', 'reply']) === false) { - showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me); - } - - if (showStatus && regex && statusForId.get('account') !== me) { - const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index'); - showStatus = !regex.test(searchIndex); - } - - return showStatus; - }); -}); - -const makeMapStateToProps = () => { - const getStatusIds = makeGetStatusIds(); - - const mapStateToProps = (state, { timelineId }) => ({ - statusIds: getStatusIds(state, { type: timelineId }), - isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), - hasMore: !!state.getIn(['timelines', timelineId, 'next']), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { timelineId, loadMore }) => ({ - - onScrollToBottom: debounce(() => { - dispatch(scrollTopTimeline(timelineId, false)); - loadMore(); - }, 300, { leading: true }), - - onScrollToTop: debounce(() => { - dispatch(scrollTopTimeline(timelineId, true)); - }, 100), - - onScroll: debounce(() => { - dispatch(scrollTopTimeline(timelineId, false)); - }, 100), - -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/javascript/themes/glitch/features/ui/index.js b/app/javascript/themes/glitch/features/ui/index.js deleted file mode 100644 index 3eea63189..000000000 --- a/app/javascript/themes/glitch/features/ui/index.js +++ /dev/null @@ -1,443 +0,0 @@ -import React from 'react'; -import NotificationsContainer from './containers/notifications_container'; -import PropTypes from 'prop-types'; -import LoadingBarContainer from './containers/loading_bar_container'; -import TabsBar from './components/tabs_bar'; -import ModalContainer from './containers/modal_container'; -import { connect } from 'react-redux'; -import { Redirect, withRouter } from 'react-router-dom'; -import { isMobile } from 'themes/glitch/util/is_mobile'; -import { debounce } from 'lodash'; -import { uploadCompose, resetCompose } from 'themes/glitch/actions/compose'; -import { refreshHomeTimeline } from 'themes/glitch/actions/timelines'; -import { refreshNotifications } from 'themes/glitch/actions/notifications'; -import { clearHeight } from 'themes/glitch/actions/height_cache'; -import { WrappedSwitch, WrappedRoute } from 'themes/glitch/util/react_router_helpers'; -import UploadArea from './components/upload_area'; -import ColumnsAreaContainer from './containers/columns_area_container'; -import classNames from 'classnames'; -import { - Compose, - Status, - GettingStarted, - PublicTimeline, - CommunityTimeline, - AccountTimeline, - AccountGallery, - HomeTimeline, - Followers, - Following, - Reblogs, - Favourites, - DirectTimeline, - HashtagTimeline, - Notifications, - FollowRequests, - GenericNotFound, - FavouritedStatuses, - Blocks, - Mutes, - PinnedStatuses, -} from 'themes/glitch/util/async-components'; -import { HotKeys } from 'react-hotkeys'; -import { me } from 'themes/glitch/util/initial_state'; -import { defineMessages, injectIntl } from 'react-intl'; - -// 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 '../../../glitch/components/status'; - -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']) !== '', - layout: state.getIn(['local_settings', 'layout']), - isWide: state.getIn(['local_settings', 'stretch']), - navbarUnder: state.getIn(['local_settings', 'navbar_under']), -}); - -const keyMap = { - 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', - toggleSpoiler: 'x', -}; - -@connect(mapStateToProps) -@injectIntl -@withRouter -export default class UI extends React.Component { - - static contextTypes = { - router: PropTypes.object.isRequired, - }; - - static propTypes = { - dispatch: PropTypes.func.isRequired, - children: PropTypes.node, - layout: PropTypes.string, - isWide: PropTypes.bool, - systemFontUi: PropTypes.bool, - navbarUnder: PropTypes.bool, - isComposing: PropTypes.bool, - hasComposingText: PropTypes.bool, - location: PropTypes.object, - intl: PropTypes.object.isRequired, - }; - - state = { - width: window.innerWidth, - draggingOver: false, - }; - - handleBeforeUnload = (e) => { - const { intl, isComposing, hasComposingText } = this.props; - - if (isComposing && hasComposingText) { - // Setting returnValue to any string causes confirmation dialog. - // Many browsers no longer display this text to users, - // but we set user-friendly message for other browsers, e.g. Edge. - e.returnValue = intl.formatMessage(messages.beforeUnload); - } - } - - handleResize = debounce(() => { - // The cached heights are no longer accurate, invalidate - this.props.dispatch(clearHeight()); - - this.setState({ width: window.innerWidth }); - }, 500, { - trailing: true, - }); - - handleDragEnter = (e) => { - e.preventDefault(); - - if (!this.dragTargets) { - this.dragTargets = []; - } - - if (this.dragTargets.indexOf(e.target) === -1) { - this.dragTargets.push(e.target); - } - - if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { - this.setState({ draggingOver: true }); - } - } - - handleDragOver = (e) => { - e.preventDefault(); - e.stopPropagation(); - - try { - e.dataTransfer.dropEffect = 'copy'; - } catch (err) { - - } - - return false; - } - - handleDrop = (e) => { - e.preventDefault(); - - this.setState({ draggingOver: false }); - - if (e.dataTransfer && e.dataTransfer.files.length === 1) { - this.props.dispatch(uploadCompose(e.dataTransfer.files)); - } - } - - handleDragLeave = (e) => { - e.preventDefault(); - e.stopPropagation(); - - this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el)); - - if (this.dragTargets.length > 0) { - return; - } - - this.setState({ draggingOver: false }); - } - - closeUploadModal = () => { - this.setState({ draggingOver: false }); - } - - handleServiceWorkerPostMessage = ({ data }) => { - if (data.type === 'navigate') { - this.context.router.history.push(data.path); - } else { - console.warn('Unknown message type:', data.type); - } - } - - componentWillMount () { - window.addEventListener('beforeunload', this.handleBeforeUnload, false); - window.addEventListener('resize', this.handleResize, { passive: true }); - document.addEventListener('dragenter', this.handleDragEnter, false); - document.addEventListener('dragover', this.handleDragOver, false); - document.addEventListener('drop', this.handleDrop, false); - document.addEventListener('dragleave', this.handleDragLeave, false); - document.addEventListener('dragend', this.handleDragEnd, false); - - if ('serviceWorker' in navigator) { - navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage); - } - - this.props.dispatch(refreshHomeTimeline()); - this.props.dispatch(refreshNotifications()); - } - - componentDidMount () { - this.hotkeys.__mousetrap__.stopCallback = (e, element) => { - return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); - }; - } - - shouldComponentUpdate (nextProps) { - if (nextProps.isComposing !== this.props.isComposing) { - // Avoid expensive update just to toggle a class - this.node.classList.toggle('is-composing', nextProps.isComposing); - this.node.classList.toggle('navbar-under', nextProps.navbarUnder); - - return false; - } - - // Why isn't this working?!? - // return super.shouldComponentUpdate(nextProps, nextState); - return true; - } - - componentDidUpdate (prevProps) { - if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { - this.columnsAreaNode.handleChildrenContentChange(); - } - } - - componentWillUnmount () { - window.removeEventListener('beforeunload', this.handleBeforeUnload); - window.removeEventListener('resize', this.handleResize); - document.removeEventListener('dragenter', this.handleDragEnter); - document.removeEventListener('dragover', this.handleDragOver); - document.removeEventListener('drop', this.handleDrop); - document.removeEventListener('dragleave', this.handleDragLeave); - document.removeEventListener('dragend', this.handleDragEnd); - } - - setRef = c => { - this.node = c; - } - - setColumnsAreaRef = c => { - this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance(); - } - - handleHotkeyNew = e => { - e.preventDefault(); - - const element = this.node.querySelector('.compose-form__autosuggest-wrapper textarea'); - - if (element) { - element.focus(); - } - } - - handleHotkeySearch = e => { - e.preventDefault(); - - const element = this.node.querySelector('.search__input'); - - 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 column = this.node.querySelector(`.column:nth-child(${index})`); - - if (column) { - const status = column.querySelector('.focusable'); - - if (status) { - status.focus(); - } - } - } - - handleHotkeyBack = () => { - if (window.history && window.history.length === 1) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - } - - setHotkeysRef = c => { - this.hotkeys = c; - } - - 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'); - } - - render () { - const { width, draggingOver } = this.state; - const { children, layout, isWide, navbarUnder } = this.props; - - const columnsClass = layout => { - switch (layout) { - case 'single': - return 'single-column'; - case 'multiple': - return 'multi-columns'; - default: - return 'auto-columns'; - } - }; - - const className = classNames('ui', columnsClass(layout), { - 'wide': isWide, - 'system-font': this.props.systemFontUi, - 'navbar-under': navbarUnder, - }); - - const handlers = { - 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, - }; - - return ( - <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}> - <div className={className} ref={this.setRef}> - {navbarUnder ? null : (<TabsBar />)} - - <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}> - <WrappedSwitch> - <Redirect from='/' to='/getting-started' exact /> - <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> - <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} /> - <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> - <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} /> - <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} /> - <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> - - <WrappedRoute path='/notifications' component={Notifications} content={children} /> - <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> - <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> - - <WrappedRoute path='/statuses/new' component={Compose} content={children} /> - <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> - <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> - <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> - - <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> - <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> - <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> - <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> - - <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> - <WrappedRoute path='/blocks' component={Blocks} content={children} /> - <WrappedRoute path='/mutes' component={Mutes} content={children} /> - - <WrappedRoute component={GenericNotFound} content={children} /> - </WrappedSwitch> - </ColumnsAreaContainer> - - <NotificationsContainer /> - {navbarUnder ? (<TabsBar />) : null} - <LoadingBarContainer className='loading-bar' /> - <ModalContainer /> - <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> - </div> - </HotKeys> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/video/index.js b/app/javascript/themes/glitch/features/video/index.js deleted file mode 100644 index 0ecbe37c9..000000000 --- a/app/javascript/themes/glitch/features/video/index.js +++ /dev/null @@ -1,288 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { throttle } from 'lodash'; -import classNames from 'classnames'; -import { isFullscreen, requestFullscreen, exitFullscreen } from 'themes/glitch/util/fullscreen'; - -const messages = defineMessages({ - play: { id: 'video.play', defaultMessage: 'Play' }, - pause: { id: 'video.pause', defaultMessage: 'Pause' }, - mute: { id: 'video.mute', defaultMessage: 'Mute sound' }, - unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, - hide: { id: 'video.hide', defaultMessage: 'Hide video' }, - expand: { id: 'video.expand', defaultMessage: 'Expand video' }, - close: { id: 'video.close', defaultMessage: 'Close video' }, - fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' }, - exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' }, -}); - -const findElementPosition = el => { - let box; - - if (el.getBoundingClientRect && el.parentNode) { - box = el.getBoundingClientRect(); - } - - if (!box) { - return { - left: 0, - top: 0, - }; - } - - const docEl = document.documentElement; - const body = document.body; - - const clientLeft = docEl.clientLeft || body.clientLeft || 0; - const scrollLeft = window.pageXOffset || body.scrollLeft; - const left = (box.left + scrollLeft) - clientLeft; - - const clientTop = docEl.clientTop || body.clientTop || 0; - const scrollTop = window.pageYOffset || body.scrollTop; - const top = (box.top + scrollTop) - clientTop; - - return { - left: Math.round(left), - top: Math.round(top), - }; -}; - -const getPointerPosition = (el, event) => { - const position = {}; - const box = findElementPosition(el); - const boxW = el.offsetWidth; - const boxH = el.offsetHeight; - const boxY = box.top; - const boxX = box.left; - - let pageY = event.pageY; - let pageX = event.pageX; - - if (event.changedTouches) { - pageX = event.changedTouches[0].pageX; - pageY = event.changedTouches[0].pageY; - } - - position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH)); - position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW)); - - return position; -}; - -@injectIntl -export default class Video extends React.PureComponent { - - static propTypes = { - preview: PropTypes.string, - src: PropTypes.string.isRequired, - alt: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - sensitive: PropTypes.bool, - startTime: PropTypes.number, - onOpenVideo: PropTypes.func, - onCloseVideo: PropTypes.func, - letterbox: PropTypes.bool, - fullwidth: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - state = { - progress: 0, - paused: true, - dragging: false, - fullscreen: false, - hovered: false, - muted: false, - revealed: !this.props.sensitive, - }; - - setPlayerRef = c => { - this.player = c; - } - - setVideoRef = c => { - this.video = c; - } - - setSeekRef = c => { - this.seek = c; - } - - handlePlay = () => { - this.setState({ paused: false }); - } - - handlePause = () => { - this.setState({ paused: true }); - } - - handleTimeUpdate = () => { - this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) }); - } - - handleMouseDown = e => { - document.addEventListener('mousemove', this.handleMouseMove, true); - document.addEventListener('mouseup', this.handleMouseUp, true); - document.addEventListener('touchmove', this.handleMouseMove, true); - document.addEventListener('touchend', this.handleMouseUp, true); - - this.setState({ dragging: true }); - this.video.pause(); - this.handleMouseMove(e); - } - - handleMouseUp = () => { - document.removeEventListener('mousemove', this.handleMouseMove, true); - document.removeEventListener('mouseup', this.handleMouseUp, true); - document.removeEventListener('touchmove', this.handleMouseMove, true); - document.removeEventListener('touchend', this.handleMouseUp, true); - - this.setState({ dragging: false }); - this.video.play(); - } - - handleMouseMove = throttle(e => { - const { x } = getPointerPosition(this.seek, e); - this.video.currentTime = this.video.duration * x; - this.setState({ progress: x * 100 }); - }, 60); - - togglePlay = () => { - if (this.state.paused) { - this.video.play(); - } else { - this.video.pause(); - } - } - - toggleFullscreen = () => { - if (isFullscreen()) { - exitFullscreen(); - } else { - requestFullscreen(this.player); - } - } - - componentDidMount () { - document.addEventListener('fullscreenchange', this.handleFullscreenChange, true); - document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); - document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); - document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); - } - - componentWillUnmount () { - document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true); - document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); - document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true); - document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true); - } - - handleFullscreenChange = () => { - this.setState({ fullscreen: isFullscreen() }); - } - - handleMouseEnter = () => { - this.setState({ hovered: true }); - } - - handleMouseLeave = () => { - this.setState({ hovered: false }); - } - - toggleMute = () => { - this.video.muted = !this.video.muted; - this.setState({ muted: this.video.muted }); - } - - toggleReveal = () => { - if (this.state.revealed) { - this.video.pause(); - } - - this.setState({ revealed: !this.state.revealed }); - } - - handleLoadedData = () => { - if (this.props.startTime) { - this.video.currentTime = this.props.startTime; - this.video.play(); - } - } - - handleProgress = () => { - if (this.video.buffered.length > 0) { - this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 }); - } - } - - handleOpenVideo = () => { - this.video.pause(); - this.props.onOpenVideo(this.video.currentTime); - } - - handleCloseVideo = () => { - this.video.pause(); - this.props.onCloseVideo(); - } - - render () { - const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth } = this.props; - const { progress, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; - - return ( - <div className={classNames('video-player', { inactive: !revealed, inline: !fullscreen, fullscreen, letterbox, 'full-width': fullwidth })} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> - <video - ref={this.setVideoRef} - src={src} - poster={preview} - preload={startTime ? 'auto' : 'none'} - loop - role='button' - tabIndex='0' - aria-label={alt} - width={width} - height={height} - onClick={this.togglePlay} - onPlay={this.handlePlay} - onPause={this.handlePause} - onTimeUpdate={this.handleTimeUpdate} - onLoadedData={this.handleLoadedData} - onProgress={this.handleProgress} - /> - - <button className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}> - <span className='video-player__spoiler__title'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> - <span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </button> - - <div className={classNames('video-player__controls', { active: paused || hovered })}> - <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> - <div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} /> - <div className='video-player__seek__progress' style={{ width: `${progress}%` }} /> - - <span - className={classNames('video-player__seek__handle', { active: dragging })} - tabIndex='0' - style={{ left: `${progress}%` }} - /> - </div> - - <div className='video-player__buttons left'> - <button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button> - <button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button> - {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} - </div> - - <div className='video-player__buttons right'> - {(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>} - {onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-times' /></button>} - <button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/index.js b/app/javascript/themes/glitch/index.js deleted file mode 100644 index 407e1f767..000000000 --- a/app/javascript/themes/glitch/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import loadPolyfills from './util/load_polyfills'; - -// import default stylesheet with variables -require('font-awesome/css/font-awesome.css'); - -import './styles/index.scss'; - -require.context('../../images/', true); - -loadPolyfills().then(() => { - require('./util/main').default(); -}).catch(e => { - console.error(e); -}); diff --git a/app/javascript/themes/glitch/middleware/errors.js b/app/javascript/themes/glitch/middleware/errors.js deleted file mode 100644 index c54e7b0a2..000000000 --- a/app/javascript/themes/glitch/middleware/errors.js +++ /dev/null @@ -1,31 +0,0 @@ -import { showAlert } from 'themes/glitch/actions/alerts'; - -const defaultFailSuffix = 'FAIL'; - -export default function errorsMiddleware() { - return ({ dispatch }) => next => action => { - if (action.type && !action.skipAlert) { - const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); - - if (action.type.match(isFail)) { - if (action.error.response) { - const { data, status, statusText } = action.error.response; - - let message = statusText; - let title = `${status}`; - - if (data.error) { - message = data.error; - } - - dispatch(showAlert(title, message)); - } else { - console.error(action.error); - dispatch(showAlert('Oops!', 'An unexpected error occurred.')); - } - } - } - - return next(action); - }; -}; diff --git a/app/javascript/themes/glitch/middleware/loading_bar.js b/app/javascript/themes/glitch/middleware/loading_bar.js deleted file mode 100644 index a98f1bb2b..000000000 --- a/app/javascript/themes/glitch/middleware/loading_bar.js +++ /dev/null @@ -1,25 +0,0 @@ -import { showLoading, hideLoading } from 'react-redux-loading-bar'; - -const defaultTypeSuffixes = ['PENDING', 'FULFILLED', 'REJECTED']; - -export default function loadingBarMiddleware(config = {}) { - const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes; - - return ({ dispatch }) => next => (action) => { - if (action.type && !action.skipLoading) { - const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes; - - const isPending = new RegExp(`${PENDING}$`, 'g'); - const isFulfilled = new RegExp(`${FULFILLED}$`, 'g'); - const isRejected = new RegExp(`${REJECTED}$`, 'g'); - - if (action.type.match(isPending)) { - dispatch(showLoading()); - } else if (action.type.match(isFulfilled) || action.type.match(isRejected)) { - dispatch(hideLoading()); - } - } - - return next(action); - }; -}; diff --git a/app/javascript/themes/glitch/middleware/sounds.js b/app/javascript/themes/glitch/middleware/sounds.js deleted file mode 100644 index 3d1e3eaba..000000000 --- a/app/javascript/themes/glitch/middleware/sounds.js +++ /dev/null @@ -1,46 +0,0 @@ -const createAudio = sources => { - const audio = new Audio(); - sources.forEach(({ type, src }) => { - const source = document.createElement('source'); - source.type = type; - source.src = src; - audio.appendChild(source); - }); - return audio; -}; - -const play = audio => { - if (!audio.paused) { - audio.pause(); - if (typeof audio.fastSeek === 'function') { - audio.fastSeek(0); - } else { - audio.seek(0); - } - } - - audio.play(); -}; - -export default function soundsMiddleware() { - const soundCache = { - boop: createAudio([ - { - src: '/sounds/boop.ogg', - type: 'audio/ogg', - }, - { - src: '/sounds/boop.mp3', - type: 'audio/mpeg', - }, - ]), - }; - - return () => next => action => { - if (action.meta && action.meta.sound && soundCache[action.meta.sound]) { - play(soundCache[action.meta.sound]); - } - - return next(action); - }; -}; diff --git a/app/javascript/themes/glitch/reducers/accounts.js b/app/javascript/themes/glitch/reducers/accounts.js deleted file mode 100644 index 0a65d3723..000000000 --- a/app/javascript/themes/glitch/reducers/accounts.js +++ /dev/null @@ -1,135 +0,0 @@ -import { - ACCOUNT_FETCH_SUCCESS, - FOLLOWERS_FETCH_SUCCESS, - FOLLOWERS_EXPAND_SUCCESS, - FOLLOWING_FETCH_SUCCESS, - FOLLOWING_EXPAND_SUCCESS, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_EXPAND_SUCCESS, -} from 'themes/glitch/actions/accounts'; -import { - BLOCKS_FETCH_SUCCESS, - BLOCKS_EXPAND_SUCCESS, -} from 'themes/glitch/actions/blocks'; -import { - MUTES_FETCH_SUCCESS, - MUTES_EXPAND_SUCCESS, -} from 'themes/glitch/actions/mutes'; -import { COMPOSE_SUGGESTIONS_READY } from 'themes/glitch/actions/compose'; -import { - REBLOG_SUCCESS, - UNREBLOG_SUCCESS, - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS, - REBLOGS_FETCH_SUCCESS, - FAVOURITES_FETCH_SUCCESS, -} from 'themes/glitch/actions/interactions'; -import { - TIMELINE_REFRESH_SUCCESS, - TIMELINE_UPDATE, - TIMELINE_EXPAND_SUCCESS, -} from 'themes/glitch/actions/timelines'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS, -} from 'themes/glitch/actions/statuses'; -import { SEARCH_FETCH_SUCCESS } from 'themes/glitch/actions/search'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS, -} from 'themes/glitch/actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from 'themes/glitch/actions/favourites'; -import { STORE_HYDRATE } from 'themes/glitch/actions/store'; -import emojify from 'themes/glitch/util/emoji'; -import { Map as ImmutableMap, fromJS } from 'immutable'; -import escapeTextContentForBrowser from 'escape-html'; - -const normalizeAccount = (state, account) => { - account = { ...account }; - - delete account.followers_count; - delete account.following_count; - delete account.statuses_count; - - const displayName = account.display_name.length === 0 ? account.username : account.display_name; - account.display_name_html = emojify(escapeTextContentForBrowser(displayName)); - account.note_emojified = emojify(account.note); - - return state.set(account.id, fromJS(account)); -}; - -const normalizeAccounts = (state, accounts) => { - accounts.forEach(account => { - state = normalizeAccount(state, account); - }); - - return state; -}; - -const normalizeAccountFromStatus = (state, status) => { - state = normalizeAccount(state, status.account); - - if (status.reblog && status.reblog.account) { - state = normalizeAccount(state, status.reblog.account); - } - - return state; -}; - -const normalizeAccountsFromStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeAccountFromStatus(state, status); - }); - - return state; -}; - -const initialState = ImmutableMap(); - -export default function accounts(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return state.merge(action.state.get('accounts')); - case ACCOUNT_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: - return normalizeAccount(state, action.account); - case FOLLOWERS_FETCH_SUCCESS: - case FOLLOWERS_EXPAND_SUCCESS: - case FOLLOWING_FETCH_SUCCESS: - case FOLLOWING_EXPAND_SUCCESS: - case REBLOGS_FETCH_SUCCESS: - case FAVOURITES_FETCH_SUCCESS: - case COMPOSE_SUGGESTIONS_READY: - case FOLLOW_REQUESTS_FETCH_SUCCESS: - case FOLLOW_REQUESTS_EXPAND_SUCCESS: - case BLOCKS_FETCH_SUCCESS: - case BLOCKS_EXPAND_SUCCESS: - case MUTES_FETCH_SUCCESS: - case MUTES_EXPAND_SUCCESS: - return action.accounts ? normalizeAccounts(state, action.accounts) : state; - case NOTIFICATIONS_REFRESH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - case SEARCH_FETCH_SUCCESS: - return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); - case TIMELINE_REFRESH_SUCCESS: - case TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - return normalizeAccountsFromStatuses(state, action.statuses); - case REBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNREBLOG_SUCCESS: - case UNFAVOURITE_SUCCESS: - return normalizeAccountFromStatus(state, action.response); - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - return normalizeAccountFromStatus(state, action.status); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/accounts_counters.js b/app/javascript/themes/glitch/reducers/accounts_counters.js deleted file mode 100644 index e3728ecd7..000000000 --- a/app/javascript/themes/glitch/reducers/accounts_counters.js +++ /dev/null @@ -1,138 +0,0 @@ -import { - ACCOUNT_FETCH_SUCCESS, - FOLLOWERS_FETCH_SUCCESS, - FOLLOWERS_EXPAND_SUCCESS, - FOLLOWING_FETCH_SUCCESS, - FOLLOWING_EXPAND_SUCCESS, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_EXPAND_SUCCESS, - ACCOUNT_FOLLOW_SUCCESS, - ACCOUNT_UNFOLLOW_SUCCESS, -} from 'themes/glitch/actions/accounts'; -import { - BLOCKS_FETCH_SUCCESS, - BLOCKS_EXPAND_SUCCESS, -} from 'themes/glitch/actions/blocks'; -import { - MUTES_FETCH_SUCCESS, - MUTES_EXPAND_SUCCESS, -} from 'themes/glitch/actions/mutes'; -import { COMPOSE_SUGGESTIONS_READY } from 'themes/glitch/actions/compose'; -import { - REBLOG_SUCCESS, - UNREBLOG_SUCCESS, - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS, - REBLOGS_FETCH_SUCCESS, - FAVOURITES_FETCH_SUCCESS, -} from 'themes/glitch/actions/interactions'; -import { - TIMELINE_REFRESH_SUCCESS, - TIMELINE_UPDATE, - TIMELINE_EXPAND_SUCCESS, -} from 'themes/glitch/actions/timelines'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS, -} from 'themes/glitch/actions/statuses'; -import { SEARCH_FETCH_SUCCESS } from 'themes/glitch/actions/search'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS, -} from 'themes/glitch/actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from 'themes/glitch/actions/favourites'; -import { STORE_HYDRATE } from 'themes/glitch/actions/store'; -import { Map as ImmutableMap, fromJS } from 'immutable'; - -const normalizeAccount = (state, account) => state.set(account.id, fromJS({ - followers_count: account.followers_count, - following_count: account.following_count, - statuses_count: account.statuses_count, -})); - -const normalizeAccounts = (state, accounts) => { - accounts.forEach(account => { - state = normalizeAccount(state, account); - }); - - return state; -}; - -const normalizeAccountFromStatus = (state, status) => { - state = normalizeAccount(state, status.account); - - if (status.reblog && status.reblog.account) { - state = normalizeAccount(state, status.reblog.account); - } - - return state; -}; - -const normalizeAccountsFromStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeAccountFromStatus(state, status); - }); - - return state; -}; - -const initialState = ImmutableMap(); - -export default function accountsCounters(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return state.merge(action.state.get('accounts').map(item => fromJS({ - followers_count: item.get('followers_count'), - following_count: item.get('following_count'), - statuses_count: item.get('statuses_count'), - }))); - case ACCOUNT_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: - return normalizeAccount(state, action.account); - case FOLLOWERS_FETCH_SUCCESS: - case FOLLOWERS_EXPAND_SUCCESS: - case FOLLOWING_FETCH_SUCCESS: - case FOLLOWING_EXPAND_SUCCESS: - case REBLOGS_FETCH_SUCCESS: - case FAVOURITES_FETCH_SUCCESS: - case COMPOSE_SUGGESTIONS_READY: - case FOLLOW_REQUESTS_FETCH_SUCCESS: - case FOLLOW_REQUESTS_EXPAND_SUCCESS: - case BLOCKS_FETCH_SUCCESS: - case BLOCKS_EXPAND_SUCCESS: - case MUTES_FETCH_SUCCESS: - case MUTES_EXPAND_SUCCESS: - return action.accounts ? normalizeAccounts(state, action.accounts) : state; - case NOTIFICATIONS_REFRESH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - case SEARCH_FETCH_SUCCESS: - return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); - case TIMELINE_REFRESH_SUCCESS: - case TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - return normalizeAccountsFromStatuses(state, action.statuses); - case REBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNREBLOG_SUCCESS: - case UNFAVOURITE_SUCCESS: - return normalizeAccountFromStatus(state, action.response); - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - return normalizeAccountFromStatus(state, action.status); - case ACCOUNT_FOLLOW_SUCCESS: - if (action.alreadyFollowing) { - return state; - } - return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); - case ACCOUNT_UNFOLLOW_SUCCESS: - return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1)); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/alerts.js b/app/javascript/themes/glitch/reducers/alerts.js deleted file mode 100644 index ad66b63f6..000000000 --- a/app/javascript/themes/glitch/reducers/alerts.js +++ /dev/null @@ -1,25 +0,0 @@ -import { - ALERT_SHOW, - ALERT_DISMISS, - ALERT_CLEAR, -} from 'themes/glitch/actions/alerts'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -const initialState = ImmutableList([]); - -export default function alerts(state = initialState, action) { - switch(action.type) { - case ALERT_SHOW: - return state.push(ImmutableMap({ - key: state.size > 0 ? state.last().get('key') + 1 : 0, - title: action.title, - message: action.message, - })); - case ALERT_DISMISS: - return state.filterNot(item => item.get('key') === action.alert.key); - case ALERT_CLEAR: - return state.clear(); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/cards.js b/app/javascript/themes/glitch/reducers/cards.js deleted file mode 100644 index 35be30444..000000000 --- a/app/javascript/themes/glitch/reducers/cards.js +++ /dev/null @@ -1,14 +0,0 @@ -import { STATUS_CARD_FETCH_SUCCESS } from 'themes/glitch/actions/cards'; - -import { Map as ImmutableMap, fromJS } from 'immutable'; - -const initialState = ImmutableMap(); - -export default function cards(state = initialState, action) { - switch(action.type) { - case STATUS_CARD_FETCH_SUCCESS: - return state.set(action.id, fromJS(action.card)); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/compose.js b/app/javascript/themes/glitch/reducers/compose.js deleted file mode 100644 index be359fcb4..000000000 --- a/app/javascript/themes/glitch/reducers/compose.js +++ /dev/null @@ -1,307 +0,0 @@ -import { - COMPOSE_MOUNT, - COMPOSE_UNMOUNT, - COMPOSE_CHANGE, - COMPOSE_REPLY, - COMPOSE_REPLY_CANCEL, - COMPOSE_MENTION, - COMPOSE_SUBMIT_REQUEST, - COMPOSE_SUBMIT_SUCCESS, - COMPOSE_SUBMIT_FAIL, - COMPOSE_UPLOAD_REQUEST, - COMPOSE_UPLOAD_SUCCESS, - COMPOSE_UPLOAD_FAIL, - COMPOSE_UPLOAD_UNDO, - COMPOSE_UPLOAD_PROGRESS, - COMPOSE_SUGGESTIONS_CLEAR, - COMPOSE_SUGGESTIONS_READY, - COMPOSE_SUGGESTION_SELECT, - COMPOSE_ADVANCED_OPTIONS_CHANGE, - COMPOSE_SENSITIVITY_CHANGE, - COMPOSE_SPOILERNESS_CHANGE, - COMPOSE_SPOILER_TEXT_CHANGE, - COMPOSE_VISIBILITY_CHANGE, - COMPOSE_COMPOSING_CHANGE, - COMPOSE_EMOJI_INSERT, - COMPOSE_UPLOAD_CHANGE_REQUEST, - COMPOSE_UPLOAD_CHANGE_SUCCESS, - COMPOSE_UPLOAD_CHANGE_FAIL, - COMPOSE_DOODLE_SET, - COMPOSE_RESET, -} from 'themes/glitch/actions/compose'; -import { TIMELINE_DELETE } from 'themes/glitch/actions/timelines'; -import { STORE_HYDRATE } from 'themes/glitch/actions/store'; -import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; -import uuid from 'themes/glitch/util/uuid'; -import { me } from 'themes/glitch/util/initial_state'; - -const initialState = ImmutableMap({ - mounted: false, - advanced_options: ImmutableMap({ - do_not_federate: false, - }), - sensitive: false, - spoiler: false, - spoiler_text: '', - privacy: null, - text: '', - focusDate: null, - preselectDate: null, - in_reply_to: null, - is_composing: false, - is_submitting: false, - is_uploading: false, - progress: 0, - media_attachments: ImmutableList(), - suggestion_token: null, - suggestions: ImmutableList(), - default_advanced_options: ImmutableMap({ - do_not_federate: false, - }), - default_privacy: 'public', - default_sensitive: false, - resetFileKey: Math.floor((Math.random() * 0x10000)), - idempotencyKey: null, - doodle: ImmutableMap({ - fg: 'rgb( 0, 0, 0)', - bg: 'rgb(255, 255, 255)', - swapped: false, - mode: 'draw', - size: 'normal', - weight: 2, - opacity: 1, - adaptiveStroke: true, - smoothing: false, - }), -}); - -function statusToTextMentions(state, status) { - let set = ImmutableOrderedSet([]); - - if (status.getIn(['account', 'id']) !== me) { - set = set.add(`@${status.getIn(['account', 'acct'])} `); - } - - return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join(''); -}; - -function clearAll(state) { - return state.withMutations(map => { - map.set('text', ''); - map.set('spoiler', false); - map.set('spoiler_text', ''); - map.set('is_submitting', false); - map.set('in_reply_to', null); - map.set('advanced_options', state.get('default_advanced_options')); - map.set('privacy', state.get('default_privacy')); - map.set('sensitive', false); - map.update('media_attachments', list => list.clear()); - map.set('idempotencyKey', uuid()); - }); -}; - -function appendMedia(state, media) { - const prevSize = state.get('media_attachments').size; - - return state.withMutations(map => { - map.update('media_attachments', list => list.push(media)); - map.set('is_uploading', false); - map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); - map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`); - map.set('focusDate', new Date()); - map.set('idempotencyKey', uuid()); - - if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) { - map.set('sensitive', true); - } - }); -}; - -function removeMedia(state, mediaId) { - const media = state.get('media_attachments').find(item => item.get('id') === mediaId); - const prevSize = state.get('media_attachments').size; - - return state.withMutations(map => { - map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId)); - map.update('text', text => text.replace(media.get('text_url'), '').trim()); - map.set('idempotencyKey', uuid()); - - if (prevSize === 1) { - map.set('sensitive', false); - } - }); -}; - -const insertSuggestion = (state, position, token, completion) => { - return state.withMutations(map => { - map.update('text', oldText => `${oldText.slice(0, position)}${completion}\u200B${oldText.slice(position + token.length)}`); - map.set('suggestion_token', null); - map.update('suggestions', ImmutableList(), list => list.clear()); - map.set('focusDate', new Date()); - map.set('idempotencyKey', uuid()); - }); -}; - -const insertEmoji = (state, position, emojiData) => { - const emoji = emojiData.native; - - return state.withMutations(map => { - map.update('text', oldText => `${oldText.slice(0, position)}${emoji}\u200B${oldText.slice(position)}`); - map.set('focusDate', new Date()); - map.set('idempotencyKey', uuid()); - }); -}; - -const privacyPreference = (a, b) => { - if (a === 'direct' || b === 'direct') { - return 'direct'; - } else if (a === 'private' || b === 'private') { - return 'private'; - } else if (a === 'unlisted' || b === 'unlisted') { - return 'unlisted'; - } else { - return 'public'; - } -}; - -const hydrate = (state, hydratedState) => { - state = clearAll(state.merge(hydratedState)); - - if (hydratedState.has('text')) { - state = state.set('text', hydratedState.get('text')); - } - - return state; -}; - -export default function compose(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return hydrate(state, action.state.get('compose')); - case COMPOSE_MOUNT: - return state.set('mounted', true); - case COMPOSE_UNMOUNT: - return state - .set('mounted', false) - .set('is_composing', false); - case COMPOSE_ADVANCED_OPTIONS_CHANGE: - return state - .set('advanced_options', - state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option]))) - .set('idempotencyKey', uuid()); - case COMPOSE_SENSITIVITY_CHANGE: - return state.withMutations(map => { - if (!state.get('spoiler')) { - map.set('sensitive', !state.get('sensitive')); - } - - map.set('idempotencyKey', uuid()); - }); - case COMPOSE_SPOILERNESS_CHANGE: - return state.withMutations(map => { - map.set('spoiler_text', ''); - map.set('spoiler', !state.get('spoiler')); - map.set('idempotencyKey', uuid()); - - if (!state.get('sensitive') && state.get('media_attachments').size >= 1) { - map.set('sensitive', true); - } - }); - case COMPOSE_SPOILER_TEXT_CHANGE: - return state - .set('spoiler_text', action.text) - .set('idempotencyKey', uuid()); - case COMPOSE_VISIBILITY_CHANGE: - return state - .set('privacy', action.value) - .set('idempotencyKey', uuid()); - case COMPOSE_CHANGE: - return state - .set('text', action.text) - .set('idempotencyKey', uuid()); - case COMPOSE_COMPOSING_CHANGE: - return state.set('is_composing', action.value); - case COMPOSE_REPLY: - return state.withMutations(map => { - map.set('in_reply_to', action.status.get('id')); - map.set('text', statusToTextMentions(state, action.status)); - map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); - map.set('advanced_options', new ImmutableMap({ - do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')), - })); - map.set('focusDate', new Date()); - map.set('preselectDate', new Date()); - map.set('idempotencyKey', uuid()); - - if (action.status.get('spoiler_text').length > 0) { - map.set('spoiler', true); - map.set('spoiler_text', action.status.get('spoiler_text')); - } else { - map.set('spoiler', false); - map.set('spoiler_text', ''); - } - }); - case COMPOSE_REPLY_CANCEL: - case COMPOSE_RESET: - return state.withMutations(map => { - map.set('in_reply_to', null); - map.set('text', ''); - map.set('spoiler', false); - map.set('spoiler_text', ''); - map.set('privacy', state.get('default_privacy')); - map.set('advanced_options', state.get('default_advanced_options')); - map.set('idempotencyKey', uuid()); - }); - case COMPOSE_SUBMIT_REQUEST: - case COMPOSE_UPLOAD_CHANGE_REQUEST: - return state.set('is_submitting', true); - case COMPOSE_SUBMIT_SUCCESS: - return clearAll(state); - case COMPOSE_SUBMIT_FAIL: - case COMPOSE_UPLOAD_CHANGE_FAIL: - return state.set('is_submitting', false); - case COMPOSE_UPLOAD_REQUEST: - return state.set('is_uploading', true); - case COMPOSE_UPLOAD_SUCCESS: - return appendMedia(state, fromJS(action.media)); - case COMPOSE_UPLOAD_FAIL: - return state.set('is_uploading', false); - case COMPOSE_UPLOAD_UNDO: - return removeMedia(state, action.media_id); - case COMPOSE_UPLOAD_PROGRESS: - return state.set('progress', Math.round((action.loaded / action.total) * 100)); - case COMPOSE_MENTION: - return state - .update('text', text => `${text}@${action.account.get('acct')} `) - .set('focusDate', new Date()) - .set('idempotencyKey', uuid()); - case COMPOSE_SUGGESTIONS_CLEAR: - return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); - case COMPOSE_SUGGESTIONS_READY: - return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); - case COMPOSE_SUGGESTION_SELECT: - return insertSuggestion(state, action.position, action.token, action.completion); - case TIMELINE_DELETE: - if (action.id === state.get('in_reply_to')) { - return state.set('in_reply_to', null); - } else { - return state; - } - case COMPOSE_EMOJI_INSERT: - return insertEmoji(state, action.position, action.emoji); - case COMPOSE_UPLOAD_CHANGE_SUCCESS: - return state - .set('is_submitting', false) - .update('media_attachments', list => list.map(item => { - if (item.get('id') === action.media.id) { - return item.set('description', action.media.description); - } - - return item; - })); - case COMPOSE_DOODLE_SET: - return state.mergeIn(['doodle'], action.options); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/contexts.js b/app/javascript/themes/glitch/reducers/contexts.js deleted file mode 100644 index 56c930bd5..000000000 --- a/app/javascript/themes/glitch/reducers/contexts.js +++ /dev/null @@ -1,61 +0,0 @@ -import { CONTEXT_FETCH_SUCCESS } from 'themes/glitch/actions/statuses'; -import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from 'themes/glitch/actions/timelines'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -const initialState = ImmutableMap({ - ancestors: ImmutableMap(), - descendants: ImmutableMap(), -}); - -const normalizeContext = (state, id, ancestors, descendants) => { - const ancestorsIds = ImmutableList(ancestors.map(ancestor => ancestor.id)); - const descendantsIds = ImmutableList(descendants.map(descendant => descendant.id)); - - return state.withMutations(map => { - map.setIn(['ancestors', id], ancestorsIds); - map.setIn(['descendants', id], descendantsIds); - }); -}; - -const deleteFromContexts = (state, id) => { - state.getIn(['descendants', id], ImmutableList()).forEach(descendantId => { - state = state.updateIn(['ancestors', descendantId], ImmutableList(), list => list.filterNot(itemId => itemId === id)); - }); - - state.getIn(['ancestors', id], ImmutableList()).forEach(ancestorId => { - state = state.updateIn(['descendants', ancestorId], ImmutableList(), list => list.filterNot(itemId => itemId === id)); - }); - - state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]); - - return state; -}; - -const updateContext = (state, status, references) => { - return state.update('descendants', map => { - references.forEach(parentId => { - map = map.update(parentId, ImmutableList(), list => { - if (list.includes(status.id)) { - return list; - } - - return list.push(status.id); - }); - }); - - return map; - }); -}; - -export default function contexts(state = initialState, action) { - switch(action.type) { - case CONTEXT_FETCH_SUCCESS: - return normalizeContext(state, action.id, action.ancestors, action.descendants); - case TIMELINE_DELETE: - return deleteFromContexts(state, action.id); - case TIMELINE_CONTEXT_UPDATE: - return updateContext(state, action.status, action.references); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/custom_emojis.js b/app/javascript/themes/glitch/reducers/custom_emojis.js deleted file mode 100644 index e3f1e0018..000000000 --- a/app/javascript/themes/glitch/reducers/custom_emojis.js +++ /dev/null @@ -1,16 +0,0 @@ -import { List as ImmutableList } from 'immutable'; -import { STORE_HYDRATE } from 'themes/glitch/actions/store'; -import { search as emojiSearch } from 'themes/glitch/util/emoji/emoji_mart_search_light'; -import { buildCustomEmojis } from 'themes/glitch/util/emoji'; - -const initialState = ImmutableList(); - -export default function custom_emojis(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', [])) }); - return action.state.get('custom_emojis'); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/height_cache.js b/app/javascript/themes/glitch/reducers/height_cache.js deleted file mode 100644 index 93c31b42c..000000000 --- a/app/javascript/themes/glitch/reducers/height_cache.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; -import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from 'themes/glitch/actions/height_cache'; - -const initialState = ImmutableMap(); - -const setHeight = (state, key, id, height) => { - return state.update(key, ImmutableMap(), map => map.set(id, height)); -}; - -const clearHeights = () => { - return ImmutableMap(); -}; - -export default function statuses(state = initialState, action) { - switch(action.type) { - case HEIGHT_CACHE_SET: - return setHeight(state, action.key, action.id, action.height); - case HEIGHT_CACHE_CLEAR: - return clearHeights(); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/index.js b/app/javascript/themes/glitch/reducers/index.js deleted file mode 100644 index aa748421a..000000000 --- a/app/javascript/themes/glitch/reducers/index.js +++ /dev/null @@ -1,54 +0,0 @@ -import { combineReducers } from 'redux-immutable'; -import timelines from './timelines'; -import meta from './meta'; -import alerts from './alerts'; -import { loadingBarReducer } from 'react-redux-loading-bar'; -import modal from './modal'; -import user_lists from './user_lists'; -import accounts from './accounts'; -import accounts_counters from './accounts_counters'; -import statuses from './statuses'; -import relationships from './relationships'; -import settings from './settings'; -import local_settings from './local_settings'; -import push_notifications from './push_notifications'; -import status_lists from './status_lists'; -import cards from './cards'; -import mutes from './mutes'; -import reports from './reports'; -import contexts from './contexts'; -import compose from './compose'; -import search from './search'; -import media_attachments from './media_attachments'; -import notifications from './notifications'; -import height_cache from './height_cache'; -import custom_emojis from './custom_emojis'; - -const reducers = { - timelines, - meta, - alerts, - loadingBar: loadingBarReducer, - modal, - user_lists, - status_lists, - accounts, - accounts_counters, - statuses, - relationships, - settings, - local_settings, - push_notifications, - cards, - mutes, - reports, - contexts, - compose, - search, - media_attachments, - notifications, - height_cache, - custom_emojis, -}; - -export default combineReducers(reducers); diff --git a/app/javascript/themes/glitch/reducers/local_settings.js b/app/javascript/themes/glitch/reducers/local_settings.js deleted file mode 100644 index b1ffa047e..000000000 --- a/app/javascript/themes/glitch/reducers/local_settings.js +++ /dev/null @@ -1,45 +0,0 @@ -// Package imports. -import { Map as ImmutableMap } from 'immutable'; - -// Our imports. -import { STORE_HYDRATE } from 'themes/glitch/actions/store'; -import { LOCAL_SETTING_CHANGE } from 'themes/glitch/actions/local_settings'; - -const initialState = ImmutableMap({ - layout : 'auto', - stretch : true, - navbar_under : false, - side_arm : 'none', - collapsed : ImmutableMap({ - enabled : true, - auto : ImmutableMap({ - all : false, - notifications : true, - lengthy : true, - reblogs : false, - replies : false, - media : false, - }), - backgrounds : ImmutableMap({ - user_backgrounds : false, - preview_images : false, - }), - }), - media : ImmutableMap({ - letterbox : true, - fullwidth : true, - }), -}); - -const hydrate = (state, localSettings) => state.mergeDeep(localSettings); - -export default function localSettings(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return hydrate(state, action.state.get('local_settings')); - case LOCAL_SETTING_CHANGE: - return state.setIn(action.key, action.value); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/media_attachments.js b/app/javascript/themes/glitch/reducers/media_attachments.js deleted file mode 100644 index 69a44639c..000000000 --- a/app/javascript/themes/glitch/reducers/media_attachments.js +++ /dev/null @@ -1,15 +0,0 @@ -import { STORE_HYDRATE } from 'themes/glitch/actions/store'; -import { Map as ImmutableMap } from 'immutable'; - -const initialState = ImmutableMap({ - accept_content_types: [], -}); - -export default function meta(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return state.merge(action.state.get('media_attachments')); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/meta.js b/app/javascript/themes/glitch/reducers/meta.js deleted file mode 100644 index 2249f1d78..000000000 --- a/app/javascript/themes/glitch/reducers/meta.js +++ /dev/null @@ -1,16 +0,0 @@ -import { STORE_HYDRATE } from 'themes/glitch/actions/store'; -import { Map as ImmutableMap } from 'immutable'; - -const initialState = ImmutableMap({ - streaming_api_base_url: null, - access_token: null, -}); - -export default function meta(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return state.merge(action.state.get('meta')); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/modal.js b/app/javascript/themes/glitch/reducers/modal.js deleted file mode 100644 index 97fb31203..000000000 --- a/app/javascript/themes/glitch/reducers/modal.js +++ /dev/null @@ -1,17 +0,0 @@ -import { MODAL_OPEN, MODAL_CLOSE } from 'themes/glitch/actions/modal'; - -const initialState = { - modalType: null, - modalProps: {}, -}; - -export default function modal(state = initialState, action) { - switch(action.type) { - case MODAL_OPEN: - return { modalType: action.modalType, modalProps: action.modalProps }; - case MODAL_CLOSE: - return initialState; - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/mutes.js b/app/javascript/themes/glitch/reducers/mutes.js deleted file mode 100644 index 8fe4ae0c3..000000000 --- a/app/javascript/themes/glitch/reducers/mutes.js +++ /dev/null @@ -1,29 +0,0 @@ -import Immutable from 'immutable'; - -import { - MUTES_INIT_MODAL, - MUTES_TOGGLE_HIDE_NOTIFICATIONS, -} from 'themes/glitch/actions/mutes'; - -const initialState = Immutable.Map({ - new: Immutable.Map({ - isSubmitting: false, - account: null, - notifications: true, - }), -}); - -export default function mutes(state = initialState, action) { - switch (action.type) { - case MUTES_INIT_MODAL: - return state.withMutations((state) => { - state.setIn(['new', 'isSubmitting'], false); - state.setIn(['new', 'account'], action.account); - state.setIn(['new', 'notifications'], true); - }); - case MUTES_TOGGLE_HIDE_NOTIFICATIONS: - return state.updateIn(['new', 'notifications'], (old) => !old); - default: - return state; - } -} diff --git a/app/javascript/themes/glitch/reducers/notifications.js b/app/javascript/themes/glitch/reducers/notifications.js deleted file mode 100644 index c4f505053..000000000 --- a/app/javascript/themes/glitch/reducers/notifications.js +++ /dev/null @@ -1,191 +0,0 @@ -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS, - NOTIFICATIONS_REFRESH_REQUEST, - NOTIFICATIONS_EXPAND_REQUEST, - NOTIFICATIONS_REFRESH_FAIL, - NOTIFICATIONS_EXPAND_FAIL, - NOTIFICATIONS_CLEAR, - NOTIFICATIONS_SCROLL_TOP, - NOTIFICATIONS_DELETE_MARKED_REQUEST, - NOTIFICATIONS_DELETE_MARKED_SUCCESS, - NOTIFICATION_MARK_FOR_DELETE, - NOTIFICATIONS_DELETE_MARKED_FAIL, - NOTIFICATIONS_ENTER_CLEARING_MODE, - NOTIFICATIONS_MARK_ALL_FOR_DELETE, -} from 'themes/glitch/actions/notifications'; -import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, -} from 'themes/glitch/actions/accounts'; -import { TIMELINE_DELETE } from 'themes/glitch/actions/timelines'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -const initialState = ImmutableMap({ - items: ImmutableList(), - next: null, - top: true, - unread: 0, - loaded: false, - isLoading: true, - cleaningMode: false, - // notification removal mark of new notifs loaded whilst cleaningMode is true. - markNewForDelete: false, -}); - -const notificationToMap = (state, notification) => ImmutableMap({ - id: notification.id, - type: notification.type, - account: notification.account.id, - markedForDelete: state.get('markNewForDelete'), - status: notification.status ? notification.status.id : null, -}); - -const normalizeNotification = (state, notification) => { - const top = state.get('top'); - - if (!top) { - state = state.update('unread', unread => unread + 1); - } - - return state.update('items', list => { - if (top && list.size > 40) { - list = list.take(20); - } - - return list.unshift(notificationToMap(state, notification)); - }); -}; - -const normalizeNotifications = (state, notifications, next) => { - let items = ImmutableList(); - const loaded = state.get('loaded'); - - notifications.forEach((n, i) => { - items = items.set(i, notificationToMap(state, n)); - }); - - if (state.get('next') === null) { - state = state.set('next', next); - } - - return state - .update('items', list => loaded ? items.concat(list) : list.concat(items)) - .set('loaded', true) - .set('isLoading', false); -}; - -const appendNormalizedNotifications = (state, notifications, next) => { - let items = ImmutableList(); - - notifications.forEach((n, i) => { - items = items.set(i, notificationToMap(state, n)); - }); - - return state - .update('items', list => list.concat(items)) - .set('next', next) - .set('isLoading', false); -}; - -const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item.get('account') === relationship.id)); -}; - -const updateTop = (state, top) => { - if (top) { - state = state.set('unread', 0); - } - - return state.set('top', top); -}; - -const deleteByStatus = (state, statusId) => { - return state.update('items', list => list.filterNot(item => item.get('status') === statusId)); -}; - -const markForDelete = (state, notificationId, yes) => { - return state.update('items', list => list.map(item => { - if(item.get('id') === notificationId) { - return item.set('markedForDelete', yes); - } else { - return item; - } - })); -}; - -const markAllForDelete = (state, yes) => { - return state.update('items', list => list.map(item => { - if(yes !== null) { - return item.set('markedForDelete', yes); - } else { - return item.set('markedForDelete', !item.get('markedForDelete')); - } - })); -}; - -const unmarkAllForDelete = (state) => { - return state.update('items', list => list.map(item => item.set('markedForDelete', false))); -}; - -const deleteMarkedNotifs = (state) => { - return state.update('items', list => list.filterNot(item => item.get('markedForDelete'))); -}; - -export default function notifications(state = initialState, action) { - let st; - - switch(action.type) { - case NOTIFICATIONS_REFRESH_REQUEST: - case NOTIFICATIONS_EXPAND_REQUEST: - case NOTIFICATIONS_DELETE_MARKED_REQUEST: - return state.set('isLoading', true); - case NOTIFICATIONS_DELETE_MARKED_FAIL: - case NOTIFICATIONS_REFRESH_FAIL: - case NOTIFICATIONS_EXPAND_FAIL: - return state.set('isLoading', false); - case NOTIFICATIONS_SCROLL_TOP: - return updateTop(state, action.top); - case NOTIFICATIONS_UPDATE: - return normalizeNotification(state, action.notification); - case NOTIFICATIONS_REFRESH_SUCCESS: - return normalizeNotifications(state, action.notifications, action.next); - case NOTIFICATIONS_EXPAND_SUCCESS: - return appendNormalizedNotifications(state, action.notifications, action.next); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterNotifications(state, action.relationship); - case NOTIFICATIONS_CLEAR: - return state.set('items', ImmutableList()).set('next', null); - case TIMELINE_DELETE: - return deleteByStatus(state, action.id); - - case NOTIFICATION_MARK_FOR_DELETE: - return markForDelete(state, action.id, action.yes); - - case NOTIFICATIONS_DELETE_MARKED_SUCCESS: - return deleteMarkedNotifs(state).set('isLoading', false); - - case NOTIFICATIONS_ENTER_CLEARING_MODE: - st = state.set('cleaningMode', action.yes); - if (!action.yes) { - return unmarkAllForDelete(st).set('markNewForDelete', false); - } else { - return st; - } - - case NOTIFICATIONS_MARK_ALL_FOR_DELETE: - st = state; - if (action.yes === null) { - // Toggle - this is a bit confusing, as it toggles the all-none mode - //st = st.set('markNewForDelete', !st.get('markNewForDelete')); - } else { - st = st.set('markNewForDelete', action.yes); - } - return markAllForDelete(st, action.yes); - - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/push_notifications.js b/app/javascript/themes/glitch/reducers/push_notifications.js deleted file mode 100644 index 744e4a0eb..000000000 --- a/app/javascript/themes/glitch/reducers/push_notifications.js +++ /dev/null @@ -1,51 +0,0 @@ -import { STORE_HYDRATE } from 'themes/glitch/actions/store'; -import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from 'themes/glitch/actions/push_notifications'; -import Immutable from 'immutable'; - -const initialState = Immutable.Map({ - subscription: null, - alerts: new Immutable.Map({ - follow: false, - favourite: false, - reblog: false, - mention: false, - }), - isSubscribed: false, - browserSupport: false, -}); - -export default function push_subscriptions(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: { - const push_subscription = action.state.get('push_subscription'); - - if (push_subscription) { - return state - .set('subscription', new Immutable.Map({ - id: push_subscription.get('id'), - endpoint: push_subscription.get('endpoint'), - })) - .set('alerts', push_subscription.get('alerts') || initialState.get('alerts')) - .set('isSubscribed', true); - } - - return state; - } - case SET_SUBSCRIPTION: - return state - .set('subscription', new Immutable.Map({ - id: action.subscription.id, - endpoint: action.subscription.endpoint, - })) - .set('alerts', new Immutable.Map(action.subscription.alerts)) - .set('isSubscribed', true); - case SET_BROWSER_SUPPORT: - return state.set('browserSupport', action.value); - case CLEAR_SUBSCRIPTION: - return initialState; - case ALERTS_CHANGE: - return state.setIn(action.key, action.value); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/relationships.js b/app/javascript/themes/glitch/reducers/relationships.js deleted file mode 100644 index d9135d6da..000000000 --- a/app/javascript/themes/glitch/reducers/relationships.js +++ /dev/null @@ -1,46 +0,0 @@ -import { - ACCOUNT_FOLLOW_SUCCESS, - ACCOUNT_UNFOLLOW_SUCCESS, - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_UNBLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - ACCOUNT_UNMUTE_SUCCESS, - RELATIONSHIPS_FETCH_SUCCESS, -} from 'themes/glitch/actions/accounts'; -import { - DOMAIN_BLOCK_SUCCESS, - DOMAIN_UNBLOCK_SUCCESS, -} from 'themes/glitch/actions/domain_blocks'; -import { Map as ImmutableMap, fromJS } from 'immutable'; - -const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship)); - -const normalizeRelationships = (state, relationships) => { - relationships.forEach(relationship => { - state = normalizeRelationship(state, relationship); - }); - - return state; -}; - -const initialState = ImmutableMap(); - -export default function relationships(state = initialState, action) { - switch(action.type) { - case ACCOUNT_FOLLOW_SUCCESS: - case ACCOUNT_UNFOLLOW_SUCCESS: - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_UNBLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - case ACCOUNT_UNMUTE_SUCCESS: - return normalizeRelationship(state, action.relationship); - case RELATIONSHIPS_FETCH_SUCCESS: - return normalizeRelationships(state, action.relationships); - case DOMAIN_BLOCK_SUCCESS: - return state.setIn([action.accountId, 'domain_blocking'], true); - case DOMAIN_UNBLOCK_SUCCESS: - return state.setIn([action.accountId, 'domain_blocking'], false); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/reports.js b/app/javascript/themes/glitch/reducers/reports.js deleted file mode 100644 index b714374ea..000000000 --- a/app/javascript/themes/glitch/reducers/reports.js +++ /dev/null @@ -1,60 +0,0 @@ -import { - REPORT_INIT, - REPORT_SUBMIT_REQUEST, - REPORT_SUBMIT_SUCCESS, - REPORT_SUBMIT_FAIL, - REPORT_CANCEL, - REPORT_STATUS_TOGGLE, - REPORT_COMMENT_CHANGE, -} from 'themes/glitch/actions/reports'; -import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable'; - -const initialState = ImmutableMap({ - new: ImmutableMap({ - isSubmitting: false, - account_id: null, - status_ids: ImmutableSet(), - comment: '', - }), -}); - -export default function reports(state = initialState, action) { - switch(action.type) { - case REPORT_INIT: - return state.withMutations(map => { - map.setIn(['new', 'isSubmitting'], false); - map.setIn(['new', 'account_id'], action.account.get('id')); - - if (state.getIn(['new', 'account_id']) !== action.account.get('id')) { - map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet()); - map.setIn(['new', 'comment'], ''); - } else if (action.status) { - map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); - } - }); - case REPORT_STATUS_TOGGLE: - return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => { - if (action.checked) { - return set.add(action.statusId); - } - - return set.remove(action.statusId); - }); - case REPORT_COMMENT_CHANGE: - return state.setIn(['new', 'comment'], action.comment); - case REPORT_SUBMIT_REQUEST: - return state.setIn(['new', 'isSubmitting'], true); - case REPORT_SUBMIT_FAIL: - return state.setIn(['new', 'isSubmitting'], false); - case REPORT_CANCEL: - case REPORT_SUBMIT_SUCCESS: - return state.withMutations(map => { - map.setIn(['new', 'account_id'], null); - map.setIn(['new', 'status_ids'], ImmutableSet()); - map.setIn(['new', 'comment'], ''); - map.setIn(['new', 'isSubmitting'], false); - }); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/search.js b/app/javascript/themes/glitch/reducers/search.js deleted file mode 100644 index aec9e2efb..000000000 --- a/app/javascript/themes/glitch/reducers/search.js +++ /dev/null @@ -1,42 +0,0 @@ -import { - SEARCH_CHANGE, - SEARCH_CLEAR, - SEARCH_FETCH_SUCCESS, - SEARCH_SHOW, -} from 'themes/glitch/actions/search'; -import { COMPOSE_MENTION, COMPOSE_REPLY } from 'themes/glitch/actions/compose'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -const initialState = ImmutableMap({ - value: '', - submitted: false, - hidden: false, - results: ImmutableMap(), -}); - -export default function search(state = initialState, action) { - switch(action.type) { - case SEARCH_CHANGE: - return state.set('value', action.value); - case SEARCH_CLEAR: - return state.withMutations(map => { - map.set('value', ''); - map.set('results', ImmutableMap()); - map.set('submitted', false); - map.set('hidden', false); - }); - case SEARCH_SHOW: - return state.set('hidden', false); - case COMPOSE_REPLY: - case COMPOSE_MENTION: - return state.set('hidden', true); - case SEARCH_FETCH_SUCCESS: - return state.set('results', ImmutableMap({ - accounts: ImmutableList(action.results.accounts.map(item => item.id)), - statuses: ImmutableList(action.results.statuses.map(item => item.id)), - hashtags: ImmutableList(action.results.hashtags), - })).set('submitted', true); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/settings.js b/app/javascript/themes/glitch/reducers/settings.js deleted file mode 100644 index c22bbbd8d..000000000 --- a/app/javascript/themes/glitch/reducers/settings.js +++ /dev/null @@ -1,119 +0,0 @@ -import { SETTING_CHANGE, SETTING_SAVE } from 'themes/glitch/actions/settings'; -import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from 'themes/glitch/actions/columns'; -import { STORE_HYDRATE } from 'themes/glitch/actions/store'; -import { EMOJI_USE } from 'themes/glitch/actions/emojis'; -import { Map as ImmutableMap, fromJS } from 'immutable'; -import uuid from 'themes/glitch/util/uuid'; - -const initialState = ImmutableMap({ - saved: true, - - onboarded: false, - layout: 'auto', - - skinTone: 1, - - home: ImmutableMap({ - shows: ImmutableMap({ - reblog: true, - reply: true, - }), - - regex: ImmutableMap({ - body: '', - }), - }), - - notifications: ImmutableMap({ - alerts: ImmutableMap({ - follow: true, - favourite: true, - reblog: true, - mention: true, - }), - - shows: ImmutableMap({ - follow: true, - favourite: true, - reblog: true, - mention: true, - }), - - sounds: ImmutableMap({ - follow: true, - favourite: true, - reblog: true, - mention: true, - }), - }), - - community: ImmutableMap({ - regex: ImmutableMap({ - body: '', - }), - }), - - public: ImmutableMap({ - regex: ImmutableMap({ - body: '', - }), - }), - - direct: ImmutableMap({ - regex: ImmutableMap({ - body: '', - }), - }), -}); - -const defaultColumns = fromJS([ - { id: 'COMPOSE', uuid: uuid(), params: {} }, - { id: 'HOME', uuid: uuid(), params: {} }, - { id: 'NOTIFICATIONS', uuid: uuid(), params: {} }, -]); - -const hydrate = (state, settings) => state.mergeDeep(settings).update('columns', (val = defaultColumns) => val); - -const moveColumn = (state, uuid, direction) => { - const columns = state.get('columns'); - const index = columns.findIndex(item => item.get('uuid') === uuid); - const newIndex = index + direction; - - let newColumns; - - newColumns = columns.splice(index, 1); - newColumns = newColumns.splice(newIndex, 0, columns.get(index)); - - return state - .set('columns', newColumns) - .set('saved', false); -}; - -const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false); - -export default function settings(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return hydrate(state, action.state.get('settings')); - case SETTING_CHANGE: - return state - .setIn(action.key, action.value) - .set('saved', false); - case COLUMN_ADD: - return state - .update('columns', list => list.push(fromJS({ id: action.id, uuid: uuid(), params: action.params }))) - .set('saved', false); - case COLUMN_REMOVE: - return state - .update('columns', list => list.filterNot(item => item.get('uuid') === action.uuid)) - .set('saved', false); - case COLUMN_MOVE: - return moveColumn(state, action.uuid, action.direction); - case EMOJI_USE: - return updateFrequentEmojis(state, action.emoji); - case SETTING_SAVE: - return state.set('saved', true); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/status_lists.js b/app/javascript/themes/glitch/reducers/status_lists.js deleted file mode 100644 index 8dc7d374e..000000000 --- a/app/javascript/themes/glitch/reducers/status_lists.js +++ /dev/null @@ -1,75 +0,0 @@ -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from 'themes/glitch/actions/favourites'; -import { - PINNED_STATUSES_FETCH_SUCCESS, -} from 'themes/glitch/actions/pin_statuses'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import { - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS, - PIN_SUCCESS, - UNPIN_SUCCESS, -} from 'themes/glitch/actions/interactions'; - -const initialState = ImmutableMap({ - favourites: ImmutableMap({ - next: null, - loaded: false, - items: ImmutableList(), - }), - pins: ImmutableMap({ - next: null, - loaded: false, - items: ImmutableList(), - }), -}); - -const normalizeList = (state, listType, statuses, next) => { - return state.update(listType, listMap => listMap.withMutations(map => { - map.set('next', next); - map.set('loaded', true); - map.set('items', ImmutableList(statuses.map(item => item.id))); - })); -}; - -const appendToList = (state, listType, statuses, next) => { - return state.update(listType, listMap => listMap.withMutations(map => { - map.set('next', next); - map.set('items', map.get('items').concat(statuses.map(item => item.id))); - })); -}; - -const prependOneToList = (state, listType, status) => { - return state.update(listType, listMap => listMap.withMutations(map => { - map.set('items', map.get('items').unshift(status.get('id'))); - })); -}; - -const removeOneFromList = (state, listType, status) => { - return state.update(listType, listMap => listMap.withMutations(map => { - map.set('items', map.get('items').filter(item => item !== status.get('id'))); - })); -}; - -export default function statusLists(state = initialState, action) { - switch(action.type) { - case FAVOURITED_STATUSES_FETCH_SUCCESS: - return normalizeList(state, 'favourites', action.statuses, action.next); - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - return appendToList(state, 'favourites', action.statuses, action.next); - case FAVOURITE_SUCCESS: - return prependOneToList(state, 'favourites', action.status); - case UNFAVOURITE_SUCCESS: - return removeOneFromList(state, 'favourites', action.status); - case PINNED_STATUSES_FETCH_SUCCESS: - return normalizeList(state, 'pins', action.statuses, action.next); - case PIN_SUCCESS: - return prependOneToList(state, 'pins', action.status); - case UNPIN_SUCCESS: - return removeOneFromList(state, 'pins', action.status); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/statuses.js b/app/javascript/themes/glitch/reducers/statuses.js deleted file mode 100644 index ef8086865..000000000 --- a/app/javascript/themes/glitch/reducers/statuses.js +++ /dev/null @@ -1,148 +0,0 @@ -import { - REBLOG_REQUEST, - REBLOG_SUCCESS, - REBLOG_FAIL, - UNREBLOG_SUCCESS, - FAVOURITE_REQUEST, - FAVOURITE_SUCCESS, - FAVOURITE_FAIL, - UNFAVOURITE_SUCCESS, - PIN_SUCCESS, - UNPIN_SUCCESS, -} from 'themes/glitch/actions/interactions'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS, - STATUS_MUTE_SUCCESS, - STATUS_UNMUTE_SUCCESS, -} from 'themes/glitch/actions/statuses'; -import { - TIMELINE_REFRESH_SUCCESS, - TIMELINE_UPDATE, - TIMELINE_DELETE, - TIMELINE_EXPAND_SUCCESS, -} from 'themes/glitch/actions/timelines'; -import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, -} from 'themes/glitch/actions/accounts'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS, -} from 'themes/glitch/actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from 'themes/glitch/actions/favourites'; -import { - PINNED_STATUSES_FETCH_SUCCESS, -} from 'themes/glitch/actions/pin_statuses'; -import { SEARCH_FETCH_SUCCESS } from 'themes/glitch/actions/search'; -import emojify from 'themes/glitch/util/emoji'; -import { Map as ImmutableMap, fromJS } from 'immutable'; -import escapeTextContentForBrowser from 'escape-html'; - -const domParser = new DOMParser(); - -const normalizeStatus = (state, status) => { - if (!status) { - return state; - } - - const normalStatus = { ...status }; - normalStatus.account = status.account.id; - - if (status.reblog && status.reblog.id) { - state = normalizeStatus(state, status.reblog); - normalStatus.reblog = status.reblog.id; - } - - const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n'); - - const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { - obj[`:${emoji.shortcode}:`] = emoji; - return obj; - }, {}); - - normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; - normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); - normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap); - - return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); -}; - -const normalizeStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeStatus(state, status); - }); - - return state; -}; - -const deleteStatus = (state, id, references) => { - references.forEach(ref => { - state = deleteStatus(state, ref[0], []); - }); - - return state.delete(id); -}; - -const filterStatuses = (state, relationship) => { - state.forEach(status => { - if (status.get('account') !== relationship.id) { - return; - } - - state = deleteStatus(state, status.get('id'), state.filter(item => item.get('reblog') === status.get('id'))); - }); - - return state; -}; - -const initialState = ImmutableMap(); - -export default function statuses(state = initialState, action) { - switch(action.type) { - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: - return normalizeStatus(state, action.status); - case REBLOG_SUCCESS: - case UNREBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNFAVOURITE_SUCCESS: - case PIN_SUCCESS: - case UNPIN_SUCCESS: - return normalizeStatus(state, action.response); - case FAVOURITE_REQUEST: - return state.setIn([action.status.get('id'), 'favourited'], true); - case FAVOURITE_FAIL: - return state.setIn([action.status.get('id'), 'favourited'], false); - case REBLOG_REQUEST: - return state.setIn([action.status.get('id'), 'reblogged'], true); - case REBLOG_FAIL: - return state.setIn([action.status.get('id'), 'reblogged'], false); - case STATUS_MUTE_SUCCESS: - return state.setIn([action.id, 'muted'], true); - case STATUS_UNMUTE_SUCCESS: - return state.setIn([action.id, 'muted'], false); - case TIMELINE_REFRESH_SUCCESS: - case TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case NOTIFICATIONS_REFRESH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - case PINNED_STATUSES_FETCH_SUCCESS: - case SEARCH_FETCH_SUCCESS: - return normalizeStatuses(state, action.statuses); - case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.references); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterStatuses(state, action.relationship); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/timelines.js b/app/javascript/themes/glitch/reducers/timelines.js deleted file mode 100644 index 7f19a1897..000000000 --- a/app/javascript/themes/glitch/reducers/timelines.js +++ /dev/null @@ -1,149 +0,0 @@ -import { - TIMELINE_REFRESH_REQUEST, - TIMELINE_REFRESH_SUCCESS, - TIMELINE_REFRESH_FAIL, - TIMELINE_UPDATE, - TIMELINE_DELETE, - TIMELINE_EXPAND_SUCCESS, - TIMELINE_EXPAND_REQUEST, - TIMELINE_EXPAND_FAIL, - TIMELINE_SCROLL_TOP, - TIMELINE_CONNECT, - TIMELINE_DISCONNECT, -} from 'themes/glitch/actions/timelines'; -import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - ACCOUNT_UNFOLLOW_SUCCESS, -} from 'themes/glitch/actions/accounts'; -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; - -const initialState = ImmutableMap(); - -const initialTimeline = ImmutableMap({ - unread: 0, - online: false, - top: true, - loaded: false, - isLoading: false, - next: false, - items: ImmutableList(), -}); - -const normalizeTimeline = (state, timeline, statuses, next) => { - const oldIds = state.getIn([timeline, 'items'], ImmutableList()); - const ids = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId)); - const wasLoaded = state.getIn([timeline, 'loaded']); - const hadNext = state.getIn([timeline, 'next']); - - return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - mMap.set('loaded', true); - mMap.set('isLoading', false); - if (!hadNext) mMap.set('next', next); - mMap.set('items', wasLoaded ? ids.concat(oldIds) : ids); - })); -}; - -const appendNormalizedTimeline = (state, timeline, statuses, next) => { - const oldIds = state.getIn([timeline, 'items'], ImmutableList()); - const ids = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId)); - - return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - mMap.set('isLoading', false); - mMap.set('next', next); - mMap.set('items', oldIds.concat(ids)); - })); -}; - -const updateTimeline = (state, timeline, status, references) => { - const top = state.getIn([timeline, 'top']); - const ids = state.getIn([timeline, 'items'], ImmutableList()); - const includesId = ids.includes(status.get('id')); - const unread = state.getIn([timeline, 'unread'], 0); - - if (includesId) { - return state; - } - - let newIds = ids; - - return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - if (!top) mMap.set('unread', unread + 1); - if (top && ids.size > 40) newIds = newIds.take(20); - if (status.getIn(['reblog', 'id'], null) !== null) newIds = newIds.filterNot(item => references.includes(item)); - mMap.set('items', newIds.unshift(status.get('id'))); - })); -}; - -const deleteStatus = (state, id, accountId, references) => { - state.keySeq().forEach(timeline => { - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); - }); - - // Remove reblogs of deleted status - references.forEach(ref => { - state = deleteStatus(state, ref[0], ref[1], []); - }); - - return state; -}; - -const filterTimelines = (state, relationship, statuses) => { - let references; - - statuses.forEach(status => { - if (status.get('account') !== relationship.id) { - return; - } - - references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]); - state = deleteStatus(state, status.get('id'), status.get('account'), references); - }); - - return state; -}; - -const filterTimeline = (timeline, state, relationship, statuses) => - state.updateIn([timeline, 'items'], ImmutableList(), list => - list.filterNot(statusId => - statuses.getIn([statusId, 'account']) === relationship.id - )); - -const updateTop = (state, timeline, top) => { - return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - if (top) mMap.set('unread', 0); - mMap.set('top', top); - })); -}; - -export default function timelines(state = initialState, action) { - switch(action.type) { - case TIMELINE_REFRESH_REQUEST: - case TIMELINE_EXPAND_REQUEST: - return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); - case TIMELINE_REFRESH_FAIL: - case TIMELINE_EXPAND_FAIL: - return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); - case TIMELINE_REFRESH_SUCCESS: - return normalizeTimeline(state, action.timeline, fromJS(action.statuses), action.next); - case TIMELINE_EXPAND_SUCCESS: - return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next); - case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, fromJS(action.status), action.references); - case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterTimelines(state, action.relationship, action.statuses); - case ACCOUNT_UNFOLLOW_SUCCESS: - return filterTimeline('home', state, action.relationship, action.statuses); - case TIMELINE_SCROLL_TOP: - return updateTop(state, action.timeline, action.top); - case TIMELINE_CONNECT: - return state.update(action.timeline, initialTimeline, map => map.set('online', true)); - case TIMELINE_DISCONNECT: - return state.update(action.timeline, initialTimeline, map => map.set('online', false)); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/reducers/user_lists.js b/app/javascript/themes/glitch/reducers/user_lists.js deleted file mode 100644 index 8c3a7d748..000000000 --- a/app/javascript/themes/glitch/reducers/user_lists.js +++ /dev/null @@ -1,80 +0,0 @@ -import { - FOLLOWERS_FETCH_SUCCESS, - FOLLOWERS_EXPAND_SUCCESS, - FOLLOWING_FETCH_SUCCESS, - FOLLOWING_EXPAND_SUCCESS, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_EXPAND_SUCCESS, - FOLLOW_REQUEST_AUTHORIZE_SUCCESS, - FOLLOW_REQUEST_REJECT_SUCCESS, -} from 'themes/glitch/actions/accounts'; -import { - REBLOGS_FETCH_SUCCESS, - FAVOURITES_FETCH_SUCCESS, -} from 'themes/glitch/actions/interactions'; -import { - BLOCKS_FETCH_SUCCESS, - BLOCKS_EXPAND_SUCCESS, -} from 'themes/glitch/actions/blocks'; -import { - MUTES_FETCH_SUCCESS, - MUTES_EXPAND_SUCCESS, -} from 'themes/glitch/actions/mutes'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -const initialState = ImmutableMap({ - followers: ImmutableMap(), - following: ImmutableMap(), - reblogged_by: ImmutableMap(), - favourited_by: ImmutableMap(), - follow_requests: ImmutableMap(), - blocks: ImmutableMap(), - mutes: ImmutableMap(), -}); - -const normalizeList = (state, type, id, accounts, next) => { - return state.setIn([type, id], ImmutableMap({ - next, - items: ImmutableList(accounts.map(item => item.id)), - })); -}; - -const appendToList = (state, type, id, accounts, next) => { - return state.updateIn([type, id], map => { - return map.set('next', next).update('items', list => list.concat(accounts.map(item => item.id))); - }); -}; - -export default function userLists(state = initialState, action) { - switch(action.type) { - case FOLLOWERS_FETCH_SUCCESS: - return normalizeList(state, 'followers', action.id, action.accounts, action.next); - case FOLLOWERS_EXPAND_SUCCESS: - return appendToList(state, 'followers', action.id, action.accounts, action.next); - case FOLLOWING_FETCH_SUCCESS: - return normalizeList(state, 'following', action.id, action.accounts, action.next); - case FOLLOWING_EXPAND_SUCCESS: - return appendToList(state, 'following', action.id, action.accounts, action.next); - case REBLOGS_FETCH_SUCCESS: - return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id))); - case FAVOURITES_FETCH_SUCCESS: - return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id))); - case FOLLOW_REQUESTS_FETCH_SUCCESS: - return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); - case FOLLOW_REQUESTS_EXPAND_SUCCESS: - return state.updateIn(['follow_requests', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); - case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: - case FOLLOW_REQUEST_REJECT_SUCCESS: - return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id)); - case BLOCKS_FETCH_SUCCESS: - return state.setIn(['blocks', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); - case BLOCKS_EXPAND_SUCCESS: - return state.updateIn(['blocks', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); - case MUTES_FETCH_SUCCESS: - return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); - case MUTES_EXPAND_SUCCESS: - return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); - default: - return state; - } -}; diff --git a/app/javascript/themes/glitch/selectors/index.js b/app/javascript/themes/glitch/selectors/index.js deleted file mode 100644 index d26d1b727..000000000 --- a/app/javascript/themes/glitch/selectors/index.js +++ /dev/null @@ -1,87 +0,0 @@ -import { createSelector } from 'reselect'; -import { List as ImmutableList } from 'immutable'; - -const getAccountBase = (state, id) => state.getIn(['accounts', id], null); -const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); -const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null); - -export const makeGetAccount = () => { - return createSelector([getAccountBase, getAccountCounters, getAccountRelationship], (base, counters, relationship) => { - if (base === null) { - return null; - } - - return base.merge(counters).set('relationship', relationship); - }); -}; - -export const makeGetStatus = () => { - return createSelector( - [ - (state, id) => state.getIn(['statuses', id]), - (state, id) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), - (state, id) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), - (state, id) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), - ], - - (statusBase, statusReblog, accountBase, accountReblog) => { - if (!statusBase) { - return null; - } - - if (statusReblog) { - statusReblog = statusReblog.set('account', accountReblog); - } else { - statusReblog = null; - } - - return statusBase.withMutations(map => { - map.set('reblog', statusReblog); - map.set('account', accountBase); - }); - } - ); -}; - -const getAlertsBase = state => state.get('alerts'); - -export const getAlerts = createSelector([getAlertsBase], (base) => { - let arr = []; - - base.forEach(item => { - arr.push({ - message: item.get('message'), - title: item.get('title'), - key: item.get('key'), - dismissAfter: 5000, - barStyle: { - zIndex: 200, - }, - }); - }); - - return arr; -}); - -export const makeGetNotification = () => { - return createSelector([ - (_, base) => base, - (state, _, accountId) => state.getIn(['accounts', accountId]), - ], (base, account) => { - return base.set('account', account); - }); -}; - -export const getAccountGallery = createSelector([ - (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()), - state => state.get('statuses'), -], (statusIds, statuses) => { - let medias = ImmutableList(); - - statusIds.forEach(statusId => { - const status = statuses.get(statusId); - medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status))); - }); - - return medias; -}); diff --git a/app/javascript/themes/glitch/service_worker/entry.js b/app/javascript/themes/glitch/service_worker/entry.js deleted file mode 100644 index eea4cfc3c..000000000 --- a/app/javascript/themes/glitch/service_worker/entry.js +++ /dev/null @@ -1,10 +0,0 @@ -import './web_push_notifications'; - -// Cause a new version of a registered Service Worker to replace an existing one -// that is already installed, and replace the currently active worker on open pages. -self.addEventListener('install', function(event) { - event.waitUntil(self.skipWaiting()); -}); -self.addEventListener('activate', function(event) { - event.waitUntil(self.clients.claim()); -}); diff --git a/app/javascript/themes/glitch/service_worker/web_push_notifications.js b/app/javascript/themes/glitch/service_worker/web_push_notifications.js deleted file mode 100644 index f63cff335..000000000 --- a/app/javascript/themes/glitch/service_worker/web_push_notifications.js +++ /dev/null @@ -1,159 +0,0 @@ -const MAX_NOTIFICATIONS = 5; -const GROUP_TAG = 'tag'; - -// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker -const formatGroupTitle = (message, count) => message.replace('%{count}', count); - -const notify = options => - self.registration.getNotifications().then(notifications => { - if (notifications.length === MAX_NOTIFICATIONS) { - // Reached the maximum number of notifications, proceed with grouping - const group = { - title: formatGroupTitle(options.data.message, notifications.length + 1), - body: notifications - .sort((n1, n2) => n1.timestamp < n2.timestamp) - .map(notification => notification.title).join('\n'), - badge: '/badge.png', - icon: '/android-chrome-192x192.png', - tag: GROUP_TAG, - data: { - url: (new URL('/web/notifications', self.location)).href, - count: notifications.length + 1, - message: options.data.message, - }, - }; - - notifications.forEach(notification => notification.close()); - - return self.registration.showNotification(group.title, group); - } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { - // Already grouped, proceed with appending the notification to the group - const group = cloneNotification(notifications[0]); - - group.title = formatGroupTitle(group.data.message, group.data.count + 1); - group.body = `${options.title}\n${group.body}`; - group.data = { ...group.data, count: group.data.count + 1 }; - - return self.registration.showNotification(group.title, group); - } - - return self.registration.showNotification(options.title, options); - }); - -const handlePush = (event) => { - const options = event.data.json(); - - options.body = options.data.nsfw || options.data.content; - options.dir = options.data.dir; - options.image = options.image || undefined; // Null results in a network request (404) - options.timestamp = options.timestamp && new Date(options.timestamp); - - const expandAction = options.data.actions.find(action => action.todo === 'expand'); - - if (expandAction) { - options.actions = [expandAction]; - options.hiddenActions = options.data.actions.filter(action => action !== expandAction); - options.data.hiddenImage = options.image; - options.image = undefined; - } else { - options.actions = options.data.actions; - } - - event.waitUntil(notify(options)); -}; - -const cloneNotification = (notification) => { - const clone = { }; - - for(var k in notification) { - clone[k] = notification[k]; - } - - return clone; -}; - -const expandNotification = (notification) => { - const nextNotification = cloneNotification(notification); - - nextNotification.body = notification.data.content; - nextNotification.image = notification.data.hiddenImage; - nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand'); - - return self.registration.showNotification(nextNotification.title, nextNotification); -}; - -const makeRequest = (notification, action) => - fetch(action.action, { - headers: { - 'Authorization': `Bearer ${notification.data.access_token}`, - 'Content-Type': 'application/json', - }, - method: action.method, - credentials: 'include', - }); - -const findBestClient = clients => { - const focusedClient = clients.find(client => client.focused); - const visibleClient = clients.find(client => client.visibilityState === 'visible'); - - return focusedClient || visibleClient || clients[0]; -}; - -const openUrl = url => - self.clients.matchAll({ type: 'window' }).then(clientList => { - if (clientList.length !== 0) { - const webClients = clientList.filter(client => /\/web\//.test(client.url)); - - if (webClients.length !== 0) { - const client = findBestClient(webClients); - const { pathname } = new URL(url); - - if (pathname.startsWith('/web/')) { - return client.focus().then(client => client.postMessage({ - type: 'navigate', - path: pathname.slice('/web/'.length - 1), - })); - } - } else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate - const client = findBestClient(clientList); - - return client.navigate(url).then(client => client.focus()); - } - } - - return self.clients.openWindow(url); - }); - -const removeActionFromNotification = (notification, action) => { - const actions = notification.actions.filter(act => act.action !== action.action); - const nextNotification = cloneNotification(notification); - - nextNotification.actions = actions; - - return self.registration.showNotification(nextNotification.title, nextNotification); -}; - -const handleNotificationClick = (event) => { - const reactToNotificationClick = new Promise((resolve, reject) => { - if (event.action) { - const action = event.notification.data.actions.find(({ action }) => action === event.action); - - if (action.todo === 'expand') { - resolve(expandNotification(event.notification)); - } else if (action.todo === 'request') { - resolve(makeRequest(event.notification, action) - .then(() => removeActionFromNotification(event.notification, action))); - } else { - reject(`Unknown action: ${action.todo}`); - } - } else { - event.notification.close(); - resolve(openUrl(event.notification.data.url)); - } - }); - - event.waitUntil(reactToNotificationClick); -}; - -self.addEventListener('push', handlePush); -self.addEventListener('notificationclick', handleNotificationClick); diff --git a/app/javascript/themes/glitch/store/configureStore.js b/app/javascript/themes/glitch/store/configureStore.js deleted file mode 100644 index 1376d4cba..000000000 --- a/app/javascript/themes/glitch/store/configureStore.js +++ /dev/null @@ -1,15 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import appReducer from '../reducers'; -import loadingBarMiddleware from '../middleware/loading_bar'; -import errorsMiddleware from '../middleware/errors'; -import soundsMiddleware from '../middleware/sounds'; - -export default function configureStore() { - return createStore(appReducer, compose(applyMiddleware( - thunk, - loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), - errorsMiddleware(), - soundsMiddleware() - ), window.devToolsExtension ? window.devToolsExtension() : f => f)); -}; diff --git a/app/javascript/themes/glitch/styles/_mixins.scss b/app/javascript/themes/glitch/styles/_mixins.scss deleted file mode 100644 index 102723e39..000000000 --- a/app/javascript/themes/glitch/styles/_mixins.scss +++ /dev/null @@ -1,52 +0,0 @@ -@mixin avatar-radius() { - border-radius: $ui-avatar-border-size; - background: transparent no-repeat; - background-position: 50%; - background-clip: padding-box; -} - -@mixin avatar-size($size:48px) { - width: $size; - height: $size; - background-size: $size $size; -} - -@mixin single-column($media, $parent: '&') { - .auto-columns #{$parent} { - @media #{$media} { - @content; - } - } - .single-column #{$parent} { - @content; - } -} - -@mixin limited-single-column($media, $parent: '&') { - .auto-columns #{$parent}, .single-column #{$parent} { - @media #{$media} { - @content; - } - } -} - -@mixin multi-columns($media, $parent: '&') { - .auto-columns #{$parent} { - @media #{$media} { - @content; - } - } - .multi-columns #{$parent} { - @content; - } -} - -@mixin fullwidth-gallery { - &.full-width { - margin-left: -22px; - margin-right: -22px; - width: inherit; - max-width: none; - height: 250px; - } -} diff --git a/app/javascript/themes/glitch/styles/about.scss b/app/javascript/themes/glitch/styles/about.scss deleted file mode 100644 index 4ec689427..000000000 --- a/app/javascript/themes/glitch/styles/about.scss +++ /dev/null @@ -1,822 +0,0 @@ -.landing-page { - p, - li { - font-family: 'mastodon-font-sans-serif', sans-serif; - font-size: 16px; - font-weight: 400; - font-size: 16px; - line-height: 30px; - margin-bottom: 12px; - color: $ui-primary-color; - - a { - color: $ui-highlight-color; - text-decoration: underline; - } - } - - em { - display: inline; - margin: 0; - padding: 0; - font-weight: 500; - background: transparent; - font-family: inherit; - font-size: inherit; - line-height: inherit; - color: lighten($ui-primary-color, 10%); - } - - h1 { - font-family: 'mastodon-font-display', sans-serif; - font-size: 26px; - line-height: 30px; - font-weight: 500; - margin-bottom: 20px; - color: $ui-secondary-color; - - small { - font-family: 'mastodon-font-sans-serif', sans-serif; - display: block; - font-size: 18px; - font-weight: 400; - color: $ui-base-lighter-color; - } - } - - h2 { - font-family: 'mastodon-font-display', sans-serif; - font-size: 22px; - line-height: 26px; - font-weight: 500; - margin-bottom: 20px; - color: $ui-secondary-color; - } - - h3 { - font-family: 'mastodon-font-display', sans-serif; - font-size: 18px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $ui-secondary-color; - } - - h4 { - font-family: 'mastodon-font-display', sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $ui-secondary-color; - } - - h5 { - font-family: 'mastodon-font-display', sans-serif; - font-size: 14px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $ui-secondary-color; - } - - h6 { - font-family: 'mastodon-font-display', sans-serif; - font-size: 12px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $ui-secondary-color; - } - - ul, - ol { - margin-left: 20px; - - &[type='a'] { - list-style-type: lower-alpha; - } - - &[type='i'] { - list-style-type: lower-roman; - } - } - - ul { - list-style: disc; - } - - ol { - list-style: decimal; - } - - li > ol, - li > ul { - margin-top: 6px; - } - - hr { - border-color: rgba($ui-base-lighter-color, .6); - } - - .container { - width: 100%; - box-sizing: border-box; - max-width: 800px; - margin: 0 auto; - word-wrap: break-word; - } - - .header-wrapper { - padding-top: 15px; - background: $ui-base-color; - background: linear-gradient(150deg, lighten($ui-base-color, 8%), $ui-base-color); - position: relative; - - &.compact { - background: $ui-base-color; - padding-bottom: 15px; - - .hero .heading { - padding-bottom: 20px; - font-family: 'mastodon-font-sans-serif', sans-serif; - font-size: 16px; - font-weight: 400; - font-size: 16px; - line-height: 30px; - color: $ui-primary-color; - - a { - color: $ui-highlight-color; - text-decoration: underline; - } - } - } - - .mascot-container { - max-width: 800px; - margin: 0 auto; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; - } - - .mascot { - position: absolute; - bottom: -14px; - width: auto; - height: auto; - left: 60px; - z-index: 3; - } - } - - .header { - line-height: 30px; - overflow: hidden; - - .container { - display: flex; - justify-content: space-between; - } - - .links { - position: relative; - z-index: 4; - - a { - display: flex; - justify-content: center; - align-items: center; - color: $ui-primary-color; - text-decoration: none; - padding: 12px 16px; - line-height: 32px; - font-family: 'mastodon-font-display', sans-serif; - font-weight: 500; - font-size: 14px; - - &:hover { - color: $ui-secondary-color; - } - } - - .brand { - a { - padding-left: 0; - padding-right: 0; - color: $white; - } - - img { - height: 32px; - position: relative; - top: 4px; - left: -10px; - } - } - - ul { - list-style: none; - margin: 0; - - li { - display: inline-block; - vertical-align: bottom; - margin: 0; - - &:first-child a { - padding-left: 0; - } - - &:last-child a { - padding-right: 0; - } - } - } - } - - .hero { - margin-top: 50px; - align-items: center; - position: relative; - - .floats { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - - div { - position: absolute; - transition: all 0.1s linear; - animation-name: floating; - animation-iteration-count: infinite; - animation-direction: alternate; - animation-timing-function: ease-in-out; - z-index: 2; - } - - .float-1 { - width: 324px; - height: 170px; - right: -120px; - bottom: 0; - animation-duration: 3s; - background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 447.1875 234.375" height="170" width="324"><path fill="#{hex-color($ui-base-lighter-color)}" d="M21.69 233.366c-6.45-1.268-13.347-5.63-16.704-10.564-10.705-15.734-1.513-37.724 18.632-44.57l4.8-1.632.173-17.753c.146-14.77.515-19.063 2.2-25.55 6.736-25.944 24.46-46.032 47.766-54.137 11.913-4.143 19.558-5.366 34.178-5.47l13.828-.096V71.12c0-4.755 2.853-17.457 5.238-23.327 8.588-21.137 26.735-35.957 52.153-42.593 23.248-6.07 50.153-6.415 71.863-.923 11.14 2.82 25.686 9.957 33.857 16.615 19.335 15.756 31.82 41.05 35.183 71.275.59 5.305.672 5.435 3.11 4.926 11.833-2.474 30.4-3.132 40.065-1.42 24.388 4.32 40.568 19.076 47.214 43.058 2.16 7.8 3.953 23.894 3.59 32.237l-.24 5.498 5.156 1.317c6.392 1.633 14.55 7.098 18.003 12.062 1.435 2.062 3.305 6.597 4.156 10.078 1.428 5.84 1.43 6.8.04 12.44-1.807 7.318-5.672 13.252-10.872 16.694-8.508 5.63 3.756 5.33-211.916 5.216-108.56-.056-199.22-.464-201.47-.906z"/></svg>'); - } - - .float-2 { - width: 241px; - height: 100px; - right: 210px; - bottom: 0; - animation-duration: 3.5s; - animation-delay: 0.2s; - background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 536.25 222.1875" height="100" width="241"><path fill="#{hex-color($ui-base-lighter-color)}" d="M42.626 221.23c-14.104-1.174-26.442-5.133-32.825-10.534-4.194-3.548-7.684-10.66-8.868-18.075-1.934-12.102.633-22.265 7.528-29.81 7.61-8.328 19.998-12.76 39.855-14.257l8.47-.638-2.08-6.223c-4.826-14.422-6.357-24.813-6.37-43.255-.012-14.923.28-18.513 2.1-25.724 2.283-9.048 8.483-23.034 13.345-30.1 14.76-21.45 43.505-38.425 70.535-41.65 30.628-3.655 64.47 12.073 89.668 41.673l5.955 6.995 2.765-4.174c1.52-2.296 5.74-6.93 9.376-10.295 18.382-17.02 43.436-20.676 73.352-10.705 12.158 4.052 21.315 9.53 29.64 17.733 12.752 12.562 18.16 25.718 18.19 44.26l.02 10.98 2.312-3.01c15.64-20.365 42.29-20.485 62.438-.28 3.644 3.653 7.558 8.593 8.697 10.976 4.895 10.24 5.932 25.688 2.486 37.046-.76 2.507-1.388 4.816-1.393 5.13-.006.316 6.845.87 15.224 1.234 53.06 2.297 76.356 12.98 81.817 37.526 3.554 15.973-3.71 28.604-19.566 34.02-4.554 1.555-17.922 1.655-234.517 1.757-126.327.06-233.497-.21-238.154-.597z"/></svg>'); - } - - .float-3 { - width: 267px; - height: 140px; - right: 110px; - top: -30px; - animation-duration: 4s; - animation-delay: 0.5s; - background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 388.125 202.5" height="140" width="267"><path fill="#{hex-color($ui-base-lighter-color)}" d="M181.37 201.458c-17.184-1.81-36.762-8.944-49.523-18.05l-5.774-4.12-8.074 2.63c-11.468 3.738-21.382 4.962-35.815 4.422-14.79-.554-24.577-2.845-36.716-8.594-15.483-7.332-28.498-19.98-35.985-34.968C2.44 128.675-.94 108.435.9 91.356c3.362-31.234 18.197-53.698 43.63-66.074 12.803-6.23 22.384-8.55 37.655-9.122 14.433-.54 24.347.684 35.814 4.42l8.073 2.633 5.635-4.01c24.81-17.656 60.007-23.332 92.914-14.985 10.11 2.565 25.498 9.62 33.102 15.178l5.068 3.704 7.632-2.564c10.89-3.66 21.086-4.916 35.516-4.376 45.816 1.716 76.422 30.03 81.285 75.196 1.84 17.08-1.54 37.32-8.585 51.422-7.487 14.99-20.502 27.636-35.984 34.968-12.14 5.75-21.926 8.04-36.716 8.593-14.43.54-24.626-.716-35.516-4.376l-7.632-2.564-5.068 3.704c-12.844 9.387-32.714 16.488-51.545 18.42-10.607 1.09-13.916 1.08-24.81-.066z"/></svg>'); - } - } - - .heading { - position: relative; - z-index: 4; - padding-bottom: 150px; - } - - .simple_form, - .closed-registrations-message { - background: darken($ui-base-color, 4%); - width: 280px; - padding: 15px 20px; - border-radius: 4px 4px 0 0; - line-height: initial; - position: relative; - z-index: 4; - - .actions { - margin-bottom: 0; - - button, - .button, - .block-button { - margin-bottom: 0; - } - } - } - - .closed-registrations-message { - min-height: 330px; - display: flex; - flex-direction: column; - justify-content: space-between; - } - } - } - - .about-short { - background: darken($ui-base-color, 4%); - padding: 50px 0 30px; - font-family: 'mastodon-font-sans-serif', sans-serif; - font-size: 16px; - font-weight: 400; - font-size: 16px; - line-height: 30px; - color: $ui-primary-color; - - a { - color: $ui-highlight-color; - text-decoration: underline; - } - } - - .information-board { - background: darken($ui-base-color, 4%); - padding: 20px 0; - - .container { - position: relative; - padding-right: 280px + 15px; - } - - .information-board-sections { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - } - - .section { - flex: 1 0 0; - font-family: 'mastodon-font-sans-serif', sans-serif; - font-size: 16px; - line-height: 28px; - color: $primary-text-color; - text-align: right; - padding: 10px 15px; - - span, - strong { - display: block; - } - - span { - &:last-child { - color: $ui-secondary-color; - } - } - - strong { - font-weight: 500; - font-size: 32px; - line-height: 48px; - } - } - - .panel { - position: absolute; - width: 280px; - box-sizing: border-box; - background: darken($ui-base-color, 8%); - padding: 20px; - padding-top: 10px; - border-radius: 4px 4px 0 0; - right: 0; - bottom: -40px; - - .panel-header { - font-family: 'mastodon-font-display', sans-serif; - font-size: 14px; - line-height: 24px; - font-weight: 500; - color: $ui-primary-color; - padding-bottom: 5px; - margin-bottom: 15px; - border-bottom: 1px solid lighten($ui-base-color, 4%); - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - - a, - span { - font-weight: 400; - color: darken($ui-primary-color, 10%); - } - - a { - text-decoration: none; - } - } - } - - .owner { - text-align: center; - - .avatar { - @include avatar-size(80px); - margin: 0 auto; - margin-bottom: 15px; - - img { - @include avatar-radius(); - @include avatar-size(80px); - display: block; - } - } - - .name { - font-size: 14px; - - a { - display: block; - color: $primary-text-color; - text-decoration: none; - - &:hover { - .display_name { - text-decoration: underline; - } - } - } - - .username { - display: block; - color: $ui-primary-color; - } - } - } - } - - .features { - padding: 50px 0; - - .container { - display: flex; - } - - #mastodon-timeline { - display: flex; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - font-family: 'mastodon-font-sans-serif', sans-serif; - font-size: 13px; - line-height: 18px; - font-weight: 400; - color: $primary-text-color; - width: 330px; - margin-right: 30px; - flex: 0 0 auto; - background: $ui-base-color; - overflow: hidden; - border-radius: 4px; - box-shadow: 0 0 6px rgba($black, 0.1); - - .column-header { - color: inherit; - font-family: inherit; - font-size: 16px; - line-height: inherit; - font-weight: inherit; - margin: 0; - padding: 15px; - } - - .column { - padding: 0; - border-radius: 4px; - overflow: hidden; - } - - .scrollable { - height: 400px; - } - - p { - font-size: inherit; - line-height: inherit; - font-weight: inherit; - color: $primary-text-color; - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - - a { - color: $ui-secondary-color; - text-decoration: none; - } - } - } - - .about-mastodon { - max-width: 675px; - - p { - margin-bottom: 20px; - } - - .features-list { - margin-top: 20px; - - .features-list__row { - display: flex; - padding: 10px 0; - justify-content: space-between; - - &:first-child { - padding-top: 0; - } - - .visual { - flex: 0 0 auto; - display: flex; - align-items: center; - margin-left: 15px; - - .fa { - display: block; - color: $ui-primary-color; - font-size: 48px; - } - } - - .text { - font-size: 16px; - line-height: 30px; - color: $ui-primary-color; - - h6 { - font-size: inherit; - line-height: inherit; - margin-bottom: 0; - } - } - } - } - } - } - - .extended-description { - padding: 50px 0; - font-family: 'mastodon-font-sans-serif', sans-serif; - font-size: 16px; - font-weight: 400; - font-size: 16px; - line-height: 30px; - color: $ui-primary-color; - - a { - color: $ui-highlight-color; - text-decoration: underline; - } - } - - .footer-links { - padding-bottom: 50px; - text-align: right; - color: $ui-base-lighter-color; - - p { - font-size: 14px; - } - - a { - color: inherit; - text-decoration: underline; - } - } - - @media screen and (max-width: 840px) { - .container { - padding: 0 20px; - } - - .information-board { - - .container { - padding-right: 20px; - } - - .section { - text-align: center; - } - - .panel { - position: static; - margin-top: 20px; - width: 100%; - border-radius: 4px; - - .panel-header { - text-align: center; - } - } - } - - .header-wrapper .mascot { - left: 20px; - } - } - - @media screen and (max-width: 689px) { - .header-wrapper .mascot { - display: none; - } - } - - @media screen and (max-width: 675px) { - .header-wrapper { - padding-top: 0; - - &.compact { - padding-bottom: 0; - } - - &.compact .hero .heading { - text-align: initial; - } - } - - .header .container, - .features .container { - display: block; - } - - .header { - - .links { - padding-top: 15px; - background: darken($ui-base-color, 4%); - - a { - padding: 12px 8px; - } - - .nav { - display: flex; - flex-flow: row wrap; - justify-content: space-around; - } - - .brand img { - left: 0; - top: 0; - } - } - - .hero { - margin-top: 30px; - padding: 0; - - .floats { - display: none; - } - - .heading { - padding: 30px 20px; - text-align: center; - } - - .simple_form, - .closed-registrations-message { - background: darken($ui-base-color, 8%); - width: 100%; - border-radius: 0; - box-sizing: border-box; - } - } - } - - .features #mastodon-timeline { - height: 70vh; - width: 100%; - margin-bottom: 50px; - - .column { - width: 100%; - } - } - } - - .cta { - margin: 20px; - } - - &.tag-page { - .features { - padding: 30px 0; - - .container { - max-width: 820px; - - #mastodon-timeline { - margin-right: 0; - border-top-right-radius: 0; - } - - .about-mastodon { - .about-hashtag { - background: darken($ui-base-color, 4%); - padding: 0 20px 20px 30px; - border-radius: 0 5px 5px 0; - - .brand { - padding-top: 20px; - margin-bottom: 20px; - - img { - height: 48px; - width: auto; - } - } - - p { - strong { - color: $ui-secondary-color; - font-weight: 700; - } - } - - .cta { - margin: 0; - - .button { - margin-right: 4px; - } - } - } - - .features-list { - margin-left: 30px; - margin-right: 10px; - } - } - } - } - - @media screen and (max-width: 675px) { - .features { - padding: 10px 0; - - .container { - display: flex; - flex-direction: column; - - #mastodon-timeline { - order: 2; - flex: 0 0 auto; - height: 60vh; - margin-bottom: 20px; - border-top-right-radius: 4px; - } - - .about-mastodon { - order: 1; - flex: 0 0 auto; - max-width: 100%; - - .about-hashtag { - background: unset; - padding: 0; - border-radius: 0; - - .cta { - margin: 20px 0; - } - } - - .features-list { - display: none; - } - } - } - } - } - } -} - -@keyframes floating { - from { - transform: translate(0, 0); - } - - 65% { - transform: translate(0, 4px); - } - - to { - transform: translate(0, -0); - } -} diff --git a/app/javascript/themes/glitch/styles/accounts.scss b/app/javascript/themes/glitch/styles/accounts.scss deleted file mode 100644 index 2cf98c642..000000000 --- a/app/javascript/themes/glitch/styles/accounts.scss +++ /dev/null @@ -1,589 +0,0 @@ -.card { - background-color: lighten($ui-base-color, 4%); - background-size: cover; - background-position: center; - border-radius: 4px 4px 0 0; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - overflow: hidden; - position: relative; - display: flex; - - &::after { - background: rgba(darken($ui-base-color, 8%), 0.5); - display: block; - content: ""; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - z-index: 1; - } - - @media screen and (max-width: 740px) { - border-radius: 0; - box-shadow: none; - } - - .card__illustration { - padding: 60px 0; - position: relative; - flex: 1 1 auto; - display: flex; - justify-content: center; - align-items: center; - } - - .card__bio { - max-width: 260px; - flex: 1 1 auto; - display: flex; - flex-direction: column; - justify-content: space-between; - background: rgba(darken($ui-base-color, 8%), 0.8); - position: relative; - z-index: 2; - } - - &.compact { - padding: 30px 0; - border-radius: 4px; - - .avatar { - margin-bottom: 0; - - img { - object-fit: cover; - } - } - } - - .name { - display: block; - font-size: 20px; - line-height: 18px * 1.5; - color: $primary-text-color; - padding: 10px 15px; - padding-bottom: 0; - font-weight: 500; - position: relative; - z-index: 2; - margin-bottom: 30px; - overflow: hidden; - text-overflow: ellipsis; - - small { - display: block; - font-size: 14px; - color: $ui-highlight-color; - font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .avatar { - @include avatar-size(120px); - margin: 0 auto; - position: relative; - z-index: 2; - - img { - @include avatar-radius(); - @include avatar-size(120px); - display: block; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - } - } - - .controls { - position: absolute; - top: 15px; - left: 15px; - z-index: 2; - - .icon-button { - color: rgba($white, 0.8); - text-decoration: none; - font-size: 13px; - line-height: 13px; - font-weight: 500; - - .fa { - font-weight: 400; - margin-right: 5px; - } - - &:hover, - &:active, - &:focus { - color: $white; - } - } - } - - .roles { - margin-bottom: 30px; - padding: 0 15px; - } - - .details-counters { - margin-top: 30px; - display: flex; - flex-direction: row; - width: 100%; - } - - .counter { - width: 33.3%; - box-sizing: border-box; - flex: 0 0 auto; - color: $ui-primary-color; - padding: 5px 10px 0; - margin-bottom: 10px; - border-right: 1px solid lighten($ui-base-color, 4%); - cursor: default; - text-align: center; - position: relative; - - a { - display: block; - } - - &:last-child { - border-right: 0; - } - - &::after { - display: block; - content: ""; - position: absolute; - bottom: -10px; - left: 0; - width: 100%; - border-bottom: 4px solid $ui-primary-color; - opacity: 0.5; - transition: all 400ms ease; - } - - &.active { - &::after { - border-bottom: 4px solid $ui-highlight-color; - opacity: 1; - } - } - - &:hover { - &::after { - opacity: 1; - transition-duration: 100ms; - } - } - - a { - text-decoration: none; - color: inherit; - } - - .counter-label { - font-size: 12px; - display: block; - margin-bottom: 5px; - } - - .counter-number { - font-weight: 500; - font-size: 18px; - color: $primary-text-color; - font-family: 'mastodon-font-display', sans-serif; - } - } - - .bio { - font-size: 14px; - line-height: 18px; - padding: 0 15px; - color: $ui-secondary-color; - } - - .metadata { - $meta-table-border: darken($classic-highlight-color, 20%);//#174f77; - - border-collapse: collapse; - padding: 0; - margin: 15px -15px -10px -15px; - border: 0 none; - border-top: 1px solid $meta-table-border; - border-bottom: 1px solid $meta-table-border; - - td, th { - padding: 10px; - border: 0 none; - border-bottom: 1px solid $meta-table-border; - vertical-align: middle; - } - - tr:last-child { - td, th { - border-bottom: 0 none; - } - } - - td { - color: $ui-primary-color; - width:100%; // makes it stretch - padding-left: 0; - } - - th { - padding-left: 15px; - font-weight: bold; - text-align: left; - width: 94px; - color: $ui-secondary-color; - background: darken($ui-base-color, 8%); - //background: #131415; - } - - a { - color: $classic-highlight-color; - } - } - - @media screen and (max-width: 480px) { - display: block; - - .card__bio { - max-width: none; - } - - .name, - .roles { - text-align: center; - margin-bottom: 15px; - } - - .bio { - margin-bottom: 15px; - } - } -} - -.pagination { - padding: 30px 0; - text-align: center; - overflow: hidden; - - a, - .current, - .next, - .prev, - .page, - .gap { - font-size: 14px; - color: $primary-text-color; - font-weight: 500; - display: inline-block; - padding: 6px 10px; - text-decoration: none; - } - - .current { - background: $simple-background-color; - border-radius: 100px; - color: $ui-base-color; - cursor: default; - margin: 0 10px; - } - - .gap { - cursor: default; - } - - .prev, - .next { - text-transform: uppercase; - color: $ui-secondary-color; - } - - .prev { - float: left; - padding-left: 0; - - .fa { - display: inline-block; - margin-right: 5px; - } - } - - .next { - float: right; - padding-right: 0; - - .fa { - display: inline-block; - margin-left: 5px; - } - } - - .disabled { - cursor: default; - color: lighten($ui-base-color, 10%); - } - - @media screen and (max-width: 700px) { - padding: 30px 20px; - - .page { - display: none; - } - - .next, - .prev { - display: inline-block; - } - } -} - -.accounts-grid { - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - background: darken($simple-background-color, 8%); - border-radius: 0 0 4px 4px; - padding: 20px 5px; - padding-bottom: 10px; - overflow: hidden; - display: flex; - flex-wrap: wrap; - z-index: 2; - position: relative; - - @media screen and (max-width: 740px) { - border-radius: 0; - box-shadow: none; - } - - .account-grid-card { - box-sizing: border-box; - width: 335px; - background: $simple-background-color; - border-radius: 4px; - color: $ui-base-color; - margin: 0 5px 10px; - position: relative; - - @media screen and (max-width: 740px) { - width: calc(100% - 10px); - } - - .account-grid-card__header { - overflow: hidden; - height: 100px; - border-radius: 4px 4px 0 0; - background-color: lighten($ui-base-color, 4%); - background-size: cover; - background-position: center; - position: relative; - - &::after { - background: rgba(darken($ui-base-color, 8%), 0.5); - display: block; - content: ""; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - z-index: 1; - } - } - - .account-grid-card__avatar { - box-sizing: border-box; - padding: 15px; - position: absolute; - z-index: 2; - top: 100px - (40px + 2px); - left: -2px; - } - - .avatar { - @include avatar-size(80px); - - img { - display: block; - @include avatar-radius(); - @include avatar-size(80px); - border: 2px solid $simple-background-color; - background: $simple-background-color; - } - } - - .name { - padding: 15px; - padding-top: 10px; - padding-left: 15px + 80px + 15px; - - a { - display: block; - color: $ui-base-color; - text-decoration: none; - text-overflow: ellipsis; - overflow: hidden; - font-weight: 500; - - &:hover { - .display_name { - text-decoration: underline; - } - } - } - } - - .display_name { - font-size: 16px; - display: block; - text-overflow: ellipsis; - overflow: hidden; - } - - .username { - color: lighten($ui-base-color, 34%); - font-size: 14px; - font-weight: 400; - } - - .note { - padding: 10px 15px; - padding-top: 15px; - box-sizing: border-box; - color: lighten($ui-base-color, 26%); - word-wrap: break-word; - min-height: 80px; - } - } -} - -.nothing-here { - width: 100%; - display: block; - color: $ui-primary-color; - font-size: 14px; - font-weight: 500; - text-align: center; - padding: 60px 0; - padding-top: 55px; - cursor: default; -} - -.account-card { - padding: 14px 10px; - background: $simple-background-color; - border-radius: 4px; - text-align: left; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - - .detailed-status__display-name { - display: block; - overflow: hidden; - margin-bottom: 15px; - - &:last-child { - margin-bottom: 0; - } - - & > div { - @include avatar-size(48px); - float: left; - margin-right: 10px; - } - - .avatar { - @include avatar-radius(); - display: block; - } - - .display-name { - display: block; - max-width: 100%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - cursor: default; - - strong { - font-weight: 500; - color: $ui-base-color; - } - - span { - font-size: 14px; - color: $ui-primary-color; - } - } - - &:hover { - .display-name { - strong { - text-decoration: none; - } - } - } - } - - .account__header__content { - font-size: 14px; - color: $ui-base-color; - } -} - -.activity-stream-tabs { - background: $simple-background-color; - border-bottom: 1px solid $ui-secondary-color; - position: relative; - z-index: 2; - - a { - display: inline-block; - padding: 15px; - text-decoration: none; - color: $ui-highlight-color; - text-transform: uppercase; - font-weight: 500; - - &:hover, - &:active, - &:focus { - color: lighten($ui-highlight-color, 8%); - } - - &.active { - color: $ui-base-color; - cursor: default; - } - } -} - -.account-role { - display: inline-block; - padding: 4px 6px; - cursor: default; - border-radius: 3px; - font-size: 12px; - line-height: 12px; - font-weight: 500; - color: $ui-secondary-color; - background-color: rgba($ui-secondary-color, 0.1); - border: 1px solid rgba($ui-secondary-color, 0.5); - - &.moderator { - color: $success-green; - background-color: rgba($success-green, 0.1); - border-color: rgba($success-green, 0.5); - } - - &.admin { - color: lighten($error-red, 12%); - background-color: rgba(lighten($error-red, 12%), 0.1); - border-color: rgba(lighten($error-red, 12%), 0.5); - } -} diff --git a/app/javascript/themes/glitch/styles/admin.scss b/app/javascript/themes/glitch/styles/admin.scss deleted file mode 100644 index 87bc710af..000000000 --- a/app/javascript/themes/glitch/styles/admin.scss +++ /dev/null @@ -1,349 +0,0 @@ -.admin-wrapper { - display: flex; - justify-content: center; - height: 100%; - - .sidebar-wrapper { - flex: 1; - height: 100%; - background: $ui-base-color; - display: flex; - justify-content: flex-end; - } - - .sidebar { - width: 240px; - height: 100%; - padding: 0; - overflow-y: auto; - - .logo { - display: block; - margin: 40px auto; - width: 100px; - height: 100px; - } - - ul { - list-style: none; - border-radius: 4px 0 0 4px; - overflow: hidden; - margin-bottom: 20px; - - a { - display: block; - padding: 15px; - color: rgba($primary-text-color, 0.7); - text-decoration: none; - transition: all 200ms linear; - border-radius: 4px 0 0 4px; - - i.fa { - margin-right: 5px; - } - - &:hover { - color: $primary-text-color; - background-color: darken($ui-base-color, 5%); - transition: all 100ms linear; - } - - &.selected { - background: darken($ui-base-color, 2%); - border-radius: 4px 0 0; - } - } - - ul { - background: darken($ui-base-color, 4%); - border-radius: 0 0 0 4px; - margin: 0; - - a { - border: 0; - padding: 15px 35px; - - &.selected { - color: $primary-text-color; - background-color: $ui-highlight-color; - border-bottom: 0; - border-radius: 0; - - &:hover { - background-color: lighten($ui-highlight-color, 5%); - } - } - } - } - } - } - - .content-wrapper { - flex: 2; - overflow: auto; - } - - .content { - max-width: 700px; - padding: 20px 15px; - padding-top: 60px; - padding-left: 25px; - - h2 { - color: $ui-secondary-color; - font-size: 24px; - line-height: 28px; - font-weight: 400; - margin-bottom: 40px; - } - - h3 { - color: $ui-secondary-color; - font-size: 20px; - line-height: 28px; - font-weight: 400; - margin-bottom: 30px; - } - - h6 { - font-size: 16px; - color: $ui-secondary-color; - line-height: 28px; - font-weight: 400; - } - - & > p { - font-size: 14px; - line-height: 18px; - color: $ui-secondary-color; - margin-bottom: 20px; - - strong { - color: $primary-text-color; - font-weight: 500; - } - } - - hr { - margin: 20px 0; - border: 0; - background: transparent; - border-bottom: 1px solid $ui-base-color; - } - - .muted-hint { - color: $ui-primary-color; - - a { - color: $ui-highlight-color; - } - } - - .positive-hint { - color: $valid-value-color; - font-weight: 500; - } - } - - .simple_form { - max-width: 400px; - - &.edit_user, - &.new_form_admin_settings, - &.new_form_two_factor_confirmation, - &.new_form_delete_confirmation, - &.new_import, - &.new_domain_block, - &.edit_domain_block { - max-width: none; - } - - .form_two_factor_confirmation_code, - .form_delete_confirmation_password { - max-width: 400px; - } - - .actions { - max-width: 400px; - } - } - - @media screen and (max-width: 600px) { - display: block; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - - .sidebar-wrapper, - .content-wrapper { - flex: 0 0 auto; - height: auto; - overflow: initial; - } - - .sidebar { - width: 100%; - padding: 10px 0; - height: auto; - - .logo { - margin: 20px auto; - } - } - - .content { - padding-top: 20px; - } - } -} - -.filters { - display: flex; - flex-wrap: wrap; - - .filter-subset { - flex: 0 0 auto; - margin: 0 40px 10px 0; - - &:last-child { - margin-bottom: 20px; - } - - ul { - margin-top: 5px; - list-style: none; - - li { - display: inline-block; - margin-right: 5px; - } - } - - strong { - font-weight: 500; - text-transform: uppercase; - font-size: 12px; - } - - a { - display: inline-block; - color: rgba($primary-text-color, 0.7); - text-decoration: none; - text-transform: uppercase; - font-size: 12px; - font-weight: 500; - border-bottom: 2px solid $ui-base-color; - - &:hover { - color: $primary-text-color; - border-bottom: 2px solid lighten($ui-base-color, 5%); - } - - &.selected { - color: $ui-highlight-color; - border-bottom: 2px solid $ui-highlight-color; - } - } - } -} - -.report-accounts { - display: flex; - flex-wrap: wrap; - margin-bottom: 20px; -} - -.report-accounts__item { - display: flex; - flex: 250px; - flex-direction: column; - margin: 0 5px; - - & > strong { - display: block; - margin: 0 0 10px -5px; - font-weight: 500; - font-size: 14px; - line-height: 18px; - color: $ui-secondary-color; - } - - .account-card { - flex: 1 1 auto; - } -} - -.report-status, -.account-status { - display: flex; - margin-bottom: 10px; - - .activity-stream { - flex: 2 0 0; - margin-right: 20px; - max-width: calc(100% - 60px); - - .entry { - border-radius: 4px; - } - } -} - -.report-status__actions, -.account-status__actions { - flex: 0 0 auto; - display: flex; - flex-direction: column; - - .icon-button { - font-size: 24px; - width: 24px; - text-align: center; - margin-bottom: 10px; - } -} - -.batch-form-box { - display: flex; - flex-wrap: wrap; - margin-bottom: 5px; - - #form_status_batch_action { - margin: 0 5px 5px 0; - font-size: 14px; - } - - input.button { - margin: 0 5px 5px 0; - } - - .media-spoiler-toggle-buttons { - margin-left: auto; - - .button { - overflow: visible; - margin: 0 0 5px 5px; - float: right; - } - } -} - -.batch-checkbox, -.batch-checkbox-all { - display: flex; - align-items: center; - margin-right: 5px; -} - -.back-link { - margin-bottom: 10px; - font-size: 14px; - - a { - color: $classic-highlight-color; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } -} diff --git a/app/javascript/themes/glitch/styles/basics.scss b/app/javascript/themes/glitch/styles/basics.scss deleted file mode 100644 index b5d77ff63..000000000 --- a/app/javascript/themes/glitch/styles/basics.scss +++ /dev/null @@ -1,122 +0,0 @@ -body { - font-family: 'mastodon-font-sans-serif', sans-serif; - background: $ui-base-color; - background-size: cover; - background-attachment: fixed; - font-size: 13px; - line-height: 18px; - font-weight: 400; - color: $primary-text-color; - padding-bottom: 20px; - text-rendering: optimizelegibility; - font-feature-settings: "kern"; - text-size-adjust: none; - -webkit-tap-highlight-color: rgba(0,0,0,0); - -webkit-tap-highlight-color: transparent; - - &.system-font { - // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+) - // -apple-system => Safari <11 specific - // BlinkMacSystemFont => Chrome <56 on macOS specific - // Segoe UI => Windows 7/8/10 - // Oxygen => KDE - // Ubuntu => Unity/Ubuntu - // Cantarell => GNOME - // Fira Sans => Firefox OS - // Droid Sans => Older Androids (<4.0) - // Helvetica Neue => Older macOS <10.11 - // mastodon-font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0) - font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", mastodon-font-sans-serif, sans-serif; - } - - &.app-body { - position: absolute; - width: 100%; - height: 100%; - padding: 0; - background: $ui-base-color; - } - - &.about-body { - background: darken($ui-base-color, 8%); - padding-bottom: 0; - } - - &.tag-body { - background: darken($ui-base-color, 8%); - padding-bottom: 0; - } - - &.embed { - background: transparent; - margin: 0; - padding-bottom: 0; - - .container { - position: absolute; - width: 100%; - height: 100%; - overflow: hidden; - } - } - - &.admin { - background: darken($ui-base-color, 4%); - position: fixed; - width: 100%; - height: 100%; - padding: 0; - } - - &.error { - position: absolute; - text-align: center; - color: $ui-primary-color; - background: $ui-base-color; - width: 100%; - height: 100%; - padding: 0; - display: flex; - justify-content: center; - align-items: center; - - .dialog { - vertical-align: middle; - margin: 20px; - - img { - display: block; - max-width: 470px; - width: 100%; - height: auto; - margin-top: -120px; - } - - h1 { - font-size: 20px; - line-height: 28px; - font-weight: 400; - } - } - } -} - -button { - font-family: inherit; - cursor: pointer; - - &:focus { - outline: none; - } -} - -.app-holder { - &, - & > div { - display: flex; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; - } -} diff --git a/app/javascript/themes/glitch/styles/boost.scss b/app/javascript/themes/glitch/styles/boost.scss deleted file mode 100644 index b07b72f8e..000000000 --- a/app/javascript/themes/glitch/styles/boost.scss +++ /dev/null @@ -1,28 +0,0 @@ -@function hex-color($color) { - @if type-of($color) == 'color' { - $color: str-slice(ie-hex-str($color), 4); - } - @return '%23' + unquote($color) -} - -button.icon-button i.fa-retweet { - background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-base-lighter-color)}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>"); - - &:hover { - background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 33%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>"); - } -} - -// Disabled variant -button.icon-button.disabled i.fa-retweet { - &, &:hover { - background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 13%))}' stroke-width='0'/></svg>"); - } -} - -// Disabled variant for use with DMs -.status-direct button.icon-button.disabled i.fa-retweet { - &, &:hover { - background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 16%))}' stroke-width='0'/></svg>"); - } -} diff --git a/app/javascript/themes/glitch/styles/compact_header.scss b/app/javascript/themes/glitch/styles/compact_header.scss deleted file mode 100644 index 90d98cc8c..000000000 --- a/app/javascript/themes/glitch/styles/compact_header.scss +++ /dev/null @@ -1,34 +0,0 @@ -.compact-header { - h1 { - font-size: 24px; - line-height: 28px; - color: $ui-primary-color; - font-weight: 500; - margin-bottom: 20px; - padding: 0 10px; - word-wrap: break-word; - - @media screen and (max-width: 740px) { - text-align: center; - padding: 20px 10px 0; - } - - a { - color: inherit; - text-decoration: none; - } - - small { - font-weight: 400; - color: $ui-secondary-color; - } - - img { - display: inline-block; - margin-bottom: -5px; - margin-right: 15px; - width: 36px; - height: 36px; - } - } -} diff --git a/app/javascript/themes/glitch/styles/components.scss b/app/javascript/themes/glitch/styles/components.scss deleted file mode 100644 index ade8df018..000000000 --- a/app/javascript/themes/glitch/styles/components.scss +++ /dev/null @@ -1,4832 +0,0 @@ -@import 'variables'; - -.app-body { - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; -} - -.button { - background-color: darken($ui-highlight-color, 3%); - border: 10px none; - border-radius: 4px; - box-sizing: border-box; - color: $primary-text-color; - cursor: pointer; - display: inline-block; - font-family: inherit; - font-size: 14px; - font-weight: 500; - height: 36px; - letter-spacing: 0; - line-height: 36px; - overflow: hidden; - padding: 0 16px; - position: relative; - text-align: center; - text-transform: uppercase; - text-decoration: none; - text-overflow: ellipsis; - transition: all 100ms ease-in; - white-space: nowrap; - width: auto; - - &:active, - &:focus, - &:hover { - background-color: lighten($ui-highlight-color, 7%); - transition: all 200ms ease-out; - } - - &:disabled { - background-color: $ui-primary-color; - cursor: default; - } - - &.button-alternative { - font-size: 16px; - line-height: 36px; - height: auto; - color: $ui-base-color; - background: $ui-primary-color; - text-transform: none; - padding: 4px 16px; - - &:active, - &:focus, - &:hover { - background-color: lighten($ui-primary-color, 4%); - } - } - - &.button-secondary { - font-size: 16px; - line-height: 36px; - height: auto; - color: $ui-primary-color; - text-transform: none; - background: transparent; - padding: 3px 15px; - border-radius: 4px; - border: 1px solid $ui-primary-color; - - &:active, - &:focus, - &:hover { - border-color: lighten($ui-primary-color, 4%); - color: lighten($ui-primary-color, 4%); - } - } - - &.button--block { - display: block; - width: 100%; - } -} - -.column__wrapper { - display: flex; - flex: 1 1 auto; - position: relative; -} - -.column-icon { - background: lighten($ui-base-color, 4%); - color: $ui-primary-color; - cursor: pointer; - font-size: 16px; - padding: 15px; - position: absolute; - right: 0; - top: -48px; - z-index: 3; - - &:hover { - color: lighten($ui-primary-color, 7%); - } -} - -.icon-button { - display: inline-block; - padding: 0; - color: $ui-base-lighter-color; - border: none; - background: transparent; - cursor: pointer; - transition: color 100ms ease-in; - - &:hover, - &:active, - &:focus { - color: lighten($ui-base-color, 33%); - transition: color 200ms ease-out; - } - - &.disabled { - color: lighten($ui-base-color, 13%); - cursor: default; - } - - &.active { - color: $ui-highlight-color; - } - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, - &:focus, - &:active { - outline: 0 !important; - } - - &.inverted { - color: lighten($ui-base-color, 33%); - - &:hover, - &:active, - &:focus { - color: $ui-base-lighter-color; - } - - &.disabled { - color: $ui-primary-color; - } - - &.active { - color: $ui-highlight-color; - - &.disabled { - color: lighten($ui-highlight-color, 13%); - } - } - } - - &.overlayed { - box-sizing: content-box; - background: rgba($base-overlay-background, 0.6); - color: rgba($primary-text-color, 0.7); - border-radius: 4px; - padding: 2px; - - &:hover { - background: rgba($base-overlay-background, 0.9); - } - } -} - -.text-icon-button { - color: lighten($ui-base-color, 33%); - border: none; - background: transparent; - cursor: pointer; - font-weight: 600; - font-size: 11px; - padding: 0 3px; - line-height: 27px; - outline: 0; - transition: color 100ms ease-in; - - &:hover, - &:active, - &:focus { - color: $ui-base-lighter-color; - transition: color 200ms ease-out; - } - - &.disabled { - color: lighten($ui-base-color, 13%); - cursor: default; - } - - &.active { - color: $ui-highlight-color; - } - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, - &:focus, - &:active { - outline: 0 !important; - } -} - -.dropdown-menu { - position: absolute; -} - -.dropdown--active .icon-button { - color: $ui-highlight-color; -} - -.dropdown--active::after { - @media screen and (min-width: 631px) { - content: ""; - display: block; - position: absolute; - width: 0; - height: 0; - border-style: solid; - border-width: 0 4.5px 7.8px; - border-color: transparent transparent $ui-secondary-color; - bottom: 8px; - right: 104px; - } -} - -.invisible { - font-size: 0; - line-height: 0; - display: inline-block; - width: 0; - height: 0; - position: absolute; - - img, - svg { - margin: 0 !important; - border: 0 !important; - padding: 0 !important; - width: 0 !important; - height: 0 !important; - } -} - -.ellipsis { - &::after { - content: "…"; - } -} - -.lightbox .icon-button { - color: $ui-base-color; -} - -.compose-form { - padding: 10px; -} - -.compose-form__warning { - color: darken($ui-secondary-color, 65%); - margin-bottom: 15px; - background: $ui-primary-color; - box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3); - padding: 8px 10px; - border-radius: 4px; - font-size: 13px; - font-weight: 400; - - strong { - color: darken($ui-secondary-color, 65%); - font-weight: 500; - } - - a { - color: darken($ui-primary-color, 33%); - font-weight: 500; - text-decoration: underline; - - &:hover, - &:active, - &:focus { - text-decoration: none; - } - } -} - -.compose-form__modifiers { - color: $ui-base-color; - font-family: inherit; - font-size: 14px; - background: $simple-background-color; -} - -.compose-form__buttons-wrapper { - display: flex; - justify-content: space-between; -} - -.compose-form__buttons { - padding: 10px; - background: darken($simple-background-color, 8%); - box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05); - border-radius: 0 0 4px 4px; - display: flex; - - .icon-button { - box-sizing: content-box; - padding: 0 3px; - } -} - -.compose-form__buttons-separator { - border-left: 1px solid #c3c3c3; - margin: 0 3px; -} - -.compose-form__upload-button-icon { - line-height: 27px; -} - -.compose-form__sensitive-button { - display: none; - - &.compose-form__sensitive-button--visible { - display: block; - } - - .compose-form__sensitive-button__icon { - line-height: 27px; - } -} - -.compose-form__upload-wrapper { - overflow: hidden; -} - -.compose-form__uploads-wrapper { - display: flex; - flex-direction: row; - padding: 5px; - flex-wrap: wrap; -} - -.compose-form__upload { - flex: 1 1 0; - min-width: 40%; - margin: 5px; - - &-description { - position: absolute; - z-index: 2; - bottom: 0; - left: 0; - right: 0; - box-sizing: border-box; - background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent); - padding: 10px; - opacity: 0; - transition: opacity .1s ease; - - input { - background: transparent; - color: $ui-secondary-color; - border: 0; - padding: 0; - margin: 0; - width: 100%; - font-family: inherit; - font-size: 14px; - font-weight: 500; - - &:focus { - color: $white; - } - - &::placeholder { - opacity: 0.54; - color: $ui-secondary-color; - } - } - - &.active { - opacity: 1; - } - } - - .icon-button { - mix-blend-mode: difference; - } -} - -.compose-form__upload-thumbnail { - border-radius: 4px; - background-position: center; - background-size: cover; - background-repeat: no-repeat; - height: 100px; - width: 100%; -} - -.compose-form__label { - display: block; - line-height: 24px; - vertical-align: middle; - - &.with-border { - border-top: 1px solid $ui-base-color; - padding-top: 10px; - } - - .compose-form__label__text { - display: inline-block; - vertical-align: middle; - margin-bottom: 14px; - margin-left: 8px; - color: $ui-primary-color; - } -} - -.compose-form__textarea, -.follow-form__input { - background: $simple-background-color; - - &:disabled { - background: $ui-secondary-color; - } -} - -.compose-form__autosuggest-wrapper { - position: relative; - - .emoji-picker-dropdown { - position: absolute; - right: 5px; - top: 5px; - - ::-webkit-scrollbar-track:hover, - ::-webkit-scrollbar-track:active { - background-color: rgba($base-overlay-background, 0.3); - } - } -} - -.compose-form__publish { - display: flex; - justify-content: flex-end; - min-width: 0; -} - -.compose-form__publish-button-wrapper { - overflow: hidden; - padding-top: 10px; - white-space: nowrap; - display: flex; - - button { - text-overflow: unset; - } -} - -.compose-form__publish__side-arm { - padding: 0 !important; - width: 36px; - text-align: center; - margin-right: 2px; -} - -.compose-form__publish__primary { - padding: 0 10px !important; -} - -.emojione { - display: inline-block; - font-size: inherit; - vertical-align: middle; - object-fit: contain; - margin: -.2ex .15em .2ex; - width: 16px; - height: 16px; - - img { - width: auto; - } -} - -.reply-indicator { - border-radius: 4px 4px 0 0; - position: relative; - bottom: -2px; - background: $ui-primary-color; - padding: 10px; -} - -.reply-indicator__header { - margin-bottom: 5px; - overflow: hidden; -} - -.reply-indicator__cancel { - float: right; - line-height: 24px; -} - -.reply-indicator__display-name { - color: $ui-base-color; - display: block; - max-width: 100%; - line-height: 24px; - overflow: hidden; - padding-right: 25px; - text-decoration: none; -} - -.reply-indicator__display-avatar { - float: left; - margin-right: 5px; -} - -.status__content--with-action { - cursor: pointer; -} - -.status-check-box { - .status__content, - .reply-indicator__content { - color: #3a3a3a; - a { - color: #005aa9; - } - } -} - -.status__content, -.reply-indicator__content { - position: relative; - margin: 10px 0; - padding: 0 12px; - font-size: 15px; - line-height: 20px; - color: $primary-text-color; - word-wrap: break-word; - font-weight: 400; - overflow: visible; - white-space: pre-wrap; - padding-top: 5px; - - &.status__content--with-spoiler { - white-space: normal; - - .status__content__text { - white-space: pre-wrap; - } - } - - .emojione { - width: 20px; - height: 20px; - margin: -5px 0 0; - } - - p { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - } - - a { - color: $ui-secondary-color; - text-decoration: none; - - &:hover { - text-decoration: underline; - - .fa { - color: lighten($ui-base-color, 40%); - } - } - - &.mention { - &:hover { - text-decoration: none; - - span { - text-decoration: underline; - } - } - } - - .fa { - color: lighten($ui-base-color, 30%); - } - } - - .status__content__spoiler { - display: none; - - &.status__content__spoiler--visible { - display: block; - } - } -} - -.status__content__spoiler-link { - display: inline-block; - border-radius: 2px; - background: lighten($ui-base-color, 30%); - border: none; - color: lighten($ui-base-color, 8%); - font-weight: 500; - font-size: 11px; - padding: 0 5px; - text-transform: uppercase; - line-height: inherit; - cursor: pointer; - vertical-align: bottom; - - &:hover { - background: lighten($ui-base-color, 33%); - text-decoration: none; - } - - .status__content__spoiler-icon { - display: inline-block; - margin: 0 0 0 5px; - border-left: 1px solid currentColor; - padding: 0 0 0 4px; - font-size: 16px; - vertical-align: -2px; - } -} - -.status__prepend-icon-wrapper { - float: left; - margin: 0 10px 0 -58px; - width: 48px; - text-align: right; -} - -.notif-cleaning { - .status, .notification-follow { - padding-right: ($dismiss-overlay-width + 0.5rem); - } -} - -.notification-follow { - position: relative; - - // same like Status - border-bottom: 1px solid lighten($ui-base-color, 8%); - - .account { - border-bottom: 0 none; - } -} - -.focusable { - &:focus { - outline: 0; - background: lighten($ui-base-color, 4%); - - .status.status-direct { - background: lighten($ui-base-color, 12%); - } - - .detailed-status, - .detailed-status__action-bar { - background: lighten($ui-base-color, 8%); - } - } -} - -.status { - padding: 8px 10px; - position: relative; - height: auto; - min-height: 48px; - border-bottom: 1px solid lighten($ui-base-color, 8%); - cursor: default; - - @supports (-ms-overflow-style: -ms-autohiding-scrollbar) { - // Add margin to avoid Edge auto-hiding scrollbar appearing over content. - // On Edge 16 this is 16px and Edge <=15 it's 12px, so aim for 16px. - padding-right: 26px; // 10px + 16px - } - - @keyframes fade { - 0% { opacity: 0; } - 100% { opacity: 1; } - } - - opacity: 1; - animation: fade 150ms linear; - - .video-player { - margin-top: 8px; - } - - &.status-direct { - background: lighten($ui-base-color, 8%); - - .icon-button.disabled { - color: lighten($ui-base-color, 16%); - } - } - - &.light { - .status__relative-time { - color: $ui-primary-color; - } - - .status__display-name { - color: $ui-base-color; - } - - .display-name { - strong { - color: $ui-base-color; - } - - span { - color: $ui-primary-color; - } - } - - .status__content { - color: $ui-base-color; - - a { - color: $ui-highlight-color; - } - - a.status__content__spoiler-link { - color: $primary-text-color; - background: $ui-primary-color; - - &:hover { - background: lighten($ui-primary-color, 8%); - } - } - } - } - - &.collapsed { - background-position: center; - background-size: cover; - user-select: none; - - &.has-background::before { - display: block; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-image: linear-gradient(to bottom, rgba($base-shadow-color, .75), rgba($base-shadow-color, .65) 24px, rgba($base-shadow-color, .8)); - content: ""; - } - - .display-name:hover .display-name__html { - text-decoration: none; - } - - .status__content { - height: 20px; - overflow: hidden; - text-overflow: ellipsis; - - a:hover { - text-decoration: none; - } - } - } - - .notification__message { - margin: -10px -10px 10px; - } -} - -.notification-favourite { - .status.status-direct { - background: transparent; - - .icon-button.disabled { - color: lighten($ui-base-color, 13%); - } - } -} - -.status__relative-time { - display: inline-block; - margin-left: auto; - padding-left: 18px; - width: 120px; - color: $ui-base-lighter-color; - font-size: 14px; - text-align: right; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.status__display-name { - margin: 0 auto 0 0; - color: $ui-base-lighter-color; - overflow: hidden; -} - -.status__info { - display: flex; - margin: 2px 0 5px; - font-size: 15px; - line-height: 24px; -} - -.status__info__icons { - flex: none; - position: relative; - color: lighten($ui-base-color, 26%); - - .status__visibility-icon { - padding-left: 6px; - } -} - -.status-check-box { - border-bottom: 1px solid $ui-secondary-color; - display: flex; - - .status__content { - flex: 1 1 auto; - padding: 10px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - -.status-check-box-toggle { - align-items: center; - display: flex; - flex: 0 0 auto; - justify-content: center; - padding: 10px; -} - -.status__prepend { - margin: -10px -10px 10px; - color: $ui-base-lighter-color; - padding: 8px 10px 0 68px; - font-size: 14px; - position: relative; - - .status__display-name strong { - color: $ui-base-lighter-color; - } - - > span { - display: block; - overflow: hidden; - text-overflow: ellipsis; - } -} - -.status__action-bar { - align-items: center; - display: flex; - margin: 10px 4px 0; -} - -.status__action-bar-button { - float: left; - margin-right: 18px; - flex: 0 0 auto; -} - -.status__action-bar-dropdown { - float: left; - height: 23.15px; - width: 23.15px; - - // Dropdown style override for centering on the icon - .dropdown--active { - position: relative; - - .dropdown__content.dropdown__right { - left: calc(50% + 3px); - right: initial; - transform: translate(-50%, 0); - top: 22px; - } - - &::after { - right: 1px; - bottom: -2px; - } - } -} - -.detailed-status__action-bar-dropdown { - flex: 1 1 auto; - display: flex; - align-items: center; - justify-content: center; - position: relative; -} - -.detailed-status { - background: lighten($ui-base-color, 4%); - padding: 14px 10px; - - .status__content { - font-size: 19px; - line-height: 24px; - - .emojione { - width: 24px; - height: 24px; - margin: -5px 0 0; - } - } - - .video-player { - margin-top: 8px; - } -} - -.detailed-status__meta { - margin-top: 15px; - color: $ui-base-lighter-color; - font-size: 14px; - line-height: 18px; -} - -.detailed-status__action-bar { - background: lighten($ui-base-color, 4%); - border-top: 1px solid lighten($ui-base-color, 8%); - border-bottom: 1px solid lighten($ui-base-color, 8%); - display: flex; - flex-direction: row; - padding: 10px 0; -} - -.detailed-status__link { - color: inherit; - text-decoration: none; -} - -.detailed-status__favorites, -.detailed-status__reblogs { - display: inline-block; - font-weight: 500; - font-size: 12px; - margin-left: 6px; -} - -.reply-indicator__content { - color: $ui-base-color; - font-size: 14px; - - a { - color: lighten($ui-base-color, 20%); - } -} - -.account { - padding: 10px; - border-bottom: 1px solid lighten($ui-base-color, 8%); - - .account__display-name { - flex: 1 1 auto; - display: block; - color: $ui-primary-color; - overflow: hidden; - text-decoration: none; - font-size: 14px; - } -} - -.account__wrapper { - display: flex; -} - -.account__avatar-wrapper { - float: left; - margin: 6px 16px 6px 6px; -} - -.account__avatar { - @include avatar-radius(); - position: relative; - cursor: pointer; - - &-inline { - display: inline-block; - vertical-align: middle; - margin-right: 5px; - } -} - -.account__avatar-overlay { - position: relative; - @include avatar-size(48px); - - &-base { - @include avatar-radius(); - @include avatar-size(36px); - } - - &-overlay { - @include avatar-radius(); - @include avatar-size(24px); - - position: absolute; - bottom: 0; - right: 0; - z-index: 1; - } -} - -.account__relationship { - height: 18px; - padding: 12px 10px; - white-space: nowrap; -} - -.account__header__wrapper { - flex: 0 0 auto; - background: lighten($ui-base-color, 4%); -} - -.account__header { - text-align: center; - background-size: cover; - background-position: center; - position: relative; - - & > div { - background: rgba(lighten($ui-base-color, 4%), 0.9); - padding: 20px 10px; - } - - .account__header__content { - color: $ui-secondary-color; - } - - .account__avatar { - @include avatar-radius(); - @include avatar-size(90px); - display: block; - margin: 0 auto 10px; - overflow: hidden; - } - - .account__header__display-name { - color: $primary-text-color; - display: inline-block; - width: 100%; - font-size: 20px; - line-height: 27px; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - } - - .account__header__username { - color: $ui-highlight-color; - font-size: 14px; - font-weight: 400; - display: block; - margin-bottom: 10px; - overflow: hidden; - text-overflow: ellipsis; - } -} - -.account__disclaimer { - padding: 10px; - border-top: 1px solid lighten($ui-base-color, 8%); - color: $ui-base-lighter-color; - - strong { - font-weight: 500; - } - - a { - font-weight: 500; - color: inherit; - text-decoration: underline; - - &:hover, - &:focus, - &:active { - text-decoration: none; - } - } -} - -.account__header__content { - color: $ui-primary-color; - font-size: 14px; - font-weight: 400; - overflow: hidden; - word-break: normal; - word-wrap: break-word; - - p { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - } - - a { - color: inherit; - text-decoration: underline; - - &:hover { - text-decoration: none; - } - } -} - -.account__header__display-name { - .emojione { - width: 25px; - height: 25px; - } -} - -.account__metadata { - width: 100%; - font-size: 15px; - line-height: 20px; - overflow: hidden; - border-collapse: collapse; - - a { - text-decoration: none; - - &:hover{ - text-decoration: underline; - } - } - - tr { - border-top: 1px solid lighten($ui-base-color, 8%); - } - - th, td { - padding: 14px 20px; - vertical-align: middle; - - & > div { - max-height: 40px; - overflow-y: auto; - white-space: pre-wrap; - text-overflow: ellipsis; - } - } - - th { - color: $ui-primary-color; - background: lighten($ui-base-color, 13%); - font-variant: small-caps; - max-width: 120px; - - a { - color: $primary-text-color; - } - } - - td { - flex: auto; - color: $primary-text-color; - background: $ui-base-color; - - a { - color: $ui-highlight-color; - } - } -} - -.account__action-bar { - border-top: 1px solid lighten($ui-base-color, 8%); - border-bottom: 1px solid lighten($ui-base-color, 8%); - line-height: 36px; - overflow: hidden; - flex: 0 0 auto; - display: flex; -} - -.account__action-bar-dropdown { - flex: 0 1 calc(50% - 140px); - padding: 10px; - - .dropdown--active { - .dropdown__content.dropdown__right { - left: 6px; - right: initial; - } - - &::after { - bottom: initial; - margin-left: 11px; - margin-top: -7px; - right: initial; - } - } -} - -.account__action-bar-links { - display: flex; - flex: 1 1 auto; - line-height: 18px; -} - -.account__action-bar__tab { - text-decoration: none; - overflow: hidden; - flex: 0 1 80px; - border-left: 1px solid lighten($ui-base-color, 8%); - padding: 10px 5px; - - & > span { - display: block; - text-transform: uppercase; - font-size: 11px; - color: $ui-primary-color; - } - - strong { - display: block; - font-size: 15px; - font-weight: 500; - color: $primary-text-color; - } - - abbr { - color: $ui-base-lighter-color; - } -} - -.account-authorize { - padding: 14px 10px; - - .detailed-status__display-name { - display: block; - margin-bottom: 15px; - overflow: hidden; - } -} - -.account-authorize__avatar { - float: left; - margin-right: 10px; -} - -.status__display-name, -.status__relative-time, -.detailed-status__display-name, -.detailed-status__datetime, -.detailed-status__application, -.account__display-name { - text-decoration: none; -} - -.status__display-name, -.account__display-name { - strong { - color: $primary-text-color; - } -} - -.muted { - .emojione { - opacity: 0.5; - } -} - -.account__display-name strong { - display: block; - overflow: hidden; - text-overflow: ellipsis; -} - -.detailed-status__application, -.detailed-status__datetime { - color: inherit; -} - -.detailed-status__display-name { - color: $ui-secondary-color; - display: block; - line-height: 24px; - margin-bottom: 15px; - overflow: hidden; - - strong, - span { - display: block; - text-overflow: ellipsis; - overflow: hidden; - } - - strong { - font-size: 16px; - color: $primary-text-color; - } -} - -.detailed-status__display-avatar { - float: left; - margin-right: 10px; -} - -.status__avatar { - flex: none; - margin: 0 10px 0 0; - height: 48px; - width: 48px; -} - -.muted { - .status__content p, - .status__content a { - color: $ui-base-lighter-color; - } - - .status__display-name strong { - color: $ui-base-lighter-color; - } - - .status__avatar, .emojione { - opacity: 0.5; - } - - a.status__content__spoiler-link { - background: $ui-base-lighter-color; - color: lighten($ui-base-color, 4%); - - &:hover { - background: lighten($ui-base-color, 29%); - text-decoration: none; - } - } -} - -.notification__message { - padding: 8px 10px 0 68px; - cursor: default; - color: $ui-primary-color; - font-size: 15px; - position: relative; - - .fa { - color: $ui-highlight-color; - } - - > span { - display: block; - overflow: hidden; - text-overflow: ellipsis; - } -} - -.notification__favourite-icon-wrapper { - float: left; - margin: 0 10px 0 -58px; - width: 48px; - text-align: right; - - .star-icon { - color: $gold-star; - } -} - -.star-icon.active { - color: $gold-star; -} - -.notification__display-name { - color: inherit; - font-weight: 500; - text-decoration: none; - - &:hover { - color: $primary-text-color; - text-decoration: underline; - } -} - -.display-name { - display: block; - padding: 6px 0; - max-width: 100%; - height: 36px; - overflow: hidden; - - strong { - display: block; - height: 18px; - font-size: 16px; - font-weight: 500; - line-height: 18px; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - span { - display: block; - height: 18px; - font-size: 15px; - line-height: 18px; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - &:hover { - strong { - text-decoration: underline; - } - } -} - -.status__relative-time, -.detailed-status__datetime { - &:hover { - text-decoration: underline; - } -} - -.image-loader { - position: relative; - - &.image-loader--loading { - .image-loader__preview-canvas { - filter: blur(2px); - } - } - - .image-loader__img { - position: absolute; - top: 0; - left: 0; - right: 0; - max-width: 100%; - max-height: 100%; - background-image: none; - } - - &.image-loader--amorphous { - position: static; - - .image-loader__preview-canvas { - display: none; - } - - .image-loader__img { - position: static; - width: auto; - height: auto; - } - } -} - -.navigation-bar { - padding: 10px; - display: flex; - flex-shrink: 0; - cursor: default; - color: $ui-primary-color; - - strong { - color: $primary-text-color; - } - - .permalink { - text-decoration: none; - } - - .icon-button { - pointer-events: none; - opacity: 0; - } -} - -.navigation-bar__profile { - flex: 1 1 auto; - margin-left: 8px; - overflow: hidden; -} - -.navigation-bar__profile-account { - display: block; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; -} - -.navigation-bar__profile-edit { - color: inherit; - text-decoration: none; -} - -.dropdown { - display: inline-block; -} - -.dropdown__content { - display: none; - position: absolute; -} - -.dropdown-menu__separator { - border-bottom: 1px solid darken($ui-secondary-color, 8%); - margin: 5px 7px 6px; - height: 0; -} - -.dropdown-menu { - background: $ui-secondary-color; - padding: 4px 0; - border-radius: 4px; - box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); - - ul { - list-style: none; - } -} - -.dropdown-menu__arrow { - position: absolute; - width: 0; - height: 0; - border: 0 solid transparent; - - &.left { - right: -5px; - margin-top: -5px; - border-width: 5px 0 5px 5px; - border-left-color: $ui-secondary-color; - } - - &.top { - bottom: -5px; - margin-left: -13px; - border-width: 5px 7px 0; - border-top-color: $ui-secondary-color; - } - - &.bottom { - top: -5px; - margin-left: -13px; - border-width: 0 7px 5px; - border-bottom-color: $ui-secondary-color; - } - - &.right { - left: -5px; - margin-top: -5px; - border-width: 5px 5px 5px 0; - border-right-color: $ui-secondary-color; - } -} - -.dropdown-menu__item { - a { - font-size: 13px; - line-height: 18px; - display: block; - padding: 4px 14px; - box-sizing: border-box; - text-decoration: none; - background: $ui-secondary-color; - color: $ui-base-color; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &:focus, - &:hover, - &:active { - background: $ui-highlight-color; - color: $ui-secondary-color; - outline: 0; - } - } -} - -.dropdown--active .dropdown__content { - display: block; - line-height: 18px; - max-width: 311px; - right: 0; - text-align: left; - z-index: 9999; - - & > ul { - list-style: none; - background: $ui-secondary-color; - padding: 4px 0; - border-radius: 4px; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.4); - min-width: 140px; - position: relative; - } - - &.dropdown__right { - right: 0; - } - - &.dropdown__left { - & > ul { - left: -98px; - } - } - - & > ul > li > a { - font-size: 13px; - line-height: 18px; - display: block; - padding: 4px 14px; - box-sizing: border-box; - text-decoration: none; - background: $ui-secondary-color; - color: $ui-base-color; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &:focus { - outline: 0; - } - - &:hover { - background: $ui-highlight-color; - color: $ui-secondary-color; - } - } -} - -.dropdown__icon { - vertical-align: middle; -} - -.static-content { - padding: 10px; - padding-top: 20px; - color: $ui-base-lighter-color; - - h1 { - font-size: 16px; - font-weight: 500; - margin-bottom: 40px; - text-align: center; - } - - p { - font-size: 13px; - margin-bottom: 20px; - } -} - -.columns-area { - display: flex; - flex: 1 1 auto; - flex-direction: row; - justify-content: flex-start; - overflow-x: auto; - position: relative; - padding: 10px; -} - -@include limited-single-column('screen and (max-width: 360px)', $parent: null) { - .columns-area { - padding: 0; - } - - .react-swipeable-view-container .columns-area { - height: calc(100% - 20px) !important; - } -} - -.react-swipeable-view-container { - &, - .columns-area, - .drawer, - .column { - height: 100%; - } -} - -.react-swipeable-view-container > * { - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} - -.column { - width: 330px; - position: relative; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow: hidden; - - .wide & { - flex: auto; - min-width: 330px; - max-width: 400px; - } - - > .scrollable { - background: $ui-base-color; - } -} - -.ui { - flex: 0 0 auto; - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - background: darken($ui-base-color, 7%); -} - -.drawer { - width: 300px; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow-y: auto; - - .wide & { - flex: 1 1 200px; - min-width: 300px; - max-width: 400px; - } -} - -.drawer__tab { - display: block; - flex: 1 1 auto; - padding: 15px 5px 13px; - color: $ui-primary-color; - text-decoration: none; - text-align: center; - font-size: 16px; - border-bottom: 2px solid transparent; - outline: none; - cursor: pointer; -} - -.column, -.drawer { - flex: 1 1 100%; - overflow: hidden; -} - -@include limited-single-column('screen and (max-width: 360px)', $parent: null) { - .tabs-bar { - margin: 0; - } - - .search { - margin-bottom: 0; - } -} - -:root { // Overrides .wide stylings for mobile view - @include single-column('screen and (max-width: 630px)', $parent: null) { - .column, - .drawer { - flex: auto; - width: 100%; - min-width: 0; - max-width: none; - padding: 0; - } - - .columns-area { - flex-direction: column; - } - - .search__input, - .autosuggest-textarea__textarea { - font-size: 16px; - } - } -} - -@include multi-columns('screen and (min-width: 631px)', $parent: null) { - .columns-area { - padding: 0; - } - - .column, - .drawer { - padding: 10px; - padding-left: 5px; - padding-right: 5px; - - &:first-child { - padding-left: 10px; - } - - &:last-child { - padding-right: 10px; - } - } - - .columns-area > div { - .column, - .drawer { - padding-left: 5px; - padding-right: 5px; - } - } -} - -.drawer__pager { - box-sizing: border-box; - padding: 0; - flex: 1 1 auto; - position: relative; -} - -.drawer__inner { - background: lighten($ui-base-color, 13%); - box-sizing: border-box; - padding: 0; - position: absolute; - height: 100%; - width: 100%; - - &.darker { - position: absolute; - top: 0; - left: 0; - background: $ui-base-color; - width: 100%; - height: 100%; - } -} - -.pseudo-drawer { - background: lighten($ui-base-color, 13%); - font-size: 13px; - text-align: left; -} - -.drawer__header { - flex: 0 0 auto; - font-size: 16px; - background: lighten($ui-base-color, 8%); - margin-bottom: 10px; - display: flex; - flex-direction: row; - - a { - transition: background 100ms ease-in; - - &:hover { - background: lighten($ui-base-color, 3%); - transition: background 200ms ease-out; - } - } -} - -.tabs-bar { - display: flex; - background: lighten($ui-base-color, 8%); - flex: 0 0 auto; - overflow-y: auto; - margin: 10px; - margin-bottom: 0; -} - -.tabs-bar__link { - display: block; - flex: 1 1 auto; - padding: 15px 10px; - color: $primary-text-color; - text-decoration: none; - text-align: center; - font-size: 14px; - font-weight: 500; - border-bottom: 2px solid lighten($ui-base-color, 8%); - transition: all 200ms linear; - - .fa { - font-weight: 400; - font-size: 16px; - } - - &.active { - border-bottom: 2px solid $ui-highlight-color; - color: $ui-highlight-color; - } - - &:hover, - &:focus, - &:active { - @include multi-columns('screen and (min-width: 631px)') { - background: lighten($ui-base-color, 14%); - transition: all 100ms linear; - } - } - - span { - margin-left: 5px; - display: none; - } -} - -@include limited-single-column('screen and (max-width: 600px)', $parent: null) { - .tabs-bar__link { - span { - display: inline; - } - } -} - -@include multi-columns('screen and (min-width: 631px)', $parent: null) { - .tabs-bar { - display: none; - } -} - -.scrollable { - overflow-y: scroll; - 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 - contain: strict; - } -} - -.scrollable.fullscreen { - @supports(display: grid) { // hack to fix Chrome <57 - contain: none; - } -} - -.column-back-button { - background: lighten($ui-base-color, 4%); - color: $ui-highlight-color; - cursor: pointer; - flex: 0 0 auto; - font-size: 16px; - border: 0; - text-align: unset; - padding: 15px; - margin: 0; - z-index: 3; - - &:hover { - text-decoration: underline; - } -} - -.column-header__back-button { - background: lighten($ui-base-color, 4%); - border: 0; - font-family: inherit; - color: $ui-highlight-color; - cursor: pointer; - flex: 0 0 auto; - font-size: 16px; - padding: 0 5px 0 0; - z-index: 3; - - &:hover { - text-decoration: underline; - } - - &:last-child { - padding: 0 15px 0 0; - } -} - -.column-back-button__icon { - display: inline-block; - margin-right: 5px; -} - -.column-back-button--slim { - position: relative; -} - -.column-back-button--slim-button { - cursor: pointer; - flex: 0 0 auto; - font-size: 16px; - padding: 15px; - position: absolute; - right: 0; - top: -48px; -} - -.react-toggle { - display: inline-block; - position: relative; - cursor: pointer; - background-color: transparent; - border: 0; - padding: 0; - user-select: none; - -webkit-tap-highlight-color: rgba($base-overlay-background, 0); - -webkit-tap-highlight-color: transparent; -} - -.react-toggle-screenreader-only { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - -.react-toggle--disabled { - cursor: not-allowed; - opacity: 0.5; - transition: opacity 0.25s; -} - -.react-toggle-track { - width: 50px; - height: 24px; - padding: 0; - border-radius: 30px; - background-color: $ui-base-color; - transition: all 0.2s ease; -} - -.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { - background-color: darken($ui-base-color, 10%); -} - -.react-toggle--checked .react-toggle-track { - background-color: $ui-highlight-color; -} - -.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { - background-color: lighten($ui-highlight-color, 10%); -} - -.react-toggle-track-check { - position: absolute; - width: 14px; - height: 10px; - top: 0; - bottom: 0; - margin-top: auto; - margin-bottom: auto; - line-height: 0; - left: 8px; - opacity: 0; - transition: opacity 0.25s ease; -} - -.react-toggle--checked .react-toggle-track-check { - opacity: 1; - transition: opacity 0.25s ease; -} - -.react-toggle-track-x { - position: absolute; - width: 10px; - height: 10px; - top: 0; - bottom: 0; - margin-top: auto; - margin-bottom: auto; - line-height: 0; - right: 10px; - opacity: 1; - transition: opacity 0.25s ease; -} - -.react-toggle--checked .react-toggle-track-x { - opacity: 0; -} - -.react-toggle-thumb { - transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; - position: absolute; - top: 1px; - left: 1px; - width: 22px; - height: 22px; - border: 1px solid $ui-base-color; - border-radius: 50%; - background-color: darken($simple-background-color, 2%); - box-sizing: border-box; - transition: all 0.25s ease; -} - -.react-toggle--checked .react-toggle-thumb { - left: 27px; - border-color: $ui-highlight-color; -} - -.column-link { - background: lighten($ui-base-color, 8%); - color: $primary-text-color; - display: block; - font-size: 16px; - padding: 15px; - text-decoration: none; - cursor: pointer; - outline: none; - - &:hover { - background: lighten($ui-base-color, 11%); - } -} - -.column-link__icon { - display: inline-block; - margin-right: 5px; -} - -.column-subheading { - background: $ui-base-color; - color: $ui-base-lighter-color; - padding: 8px 20px; - font-size: 12px; - font-weight: 500; - text-transform: uppercase; - cursor: default; -} - -.autosuggest-textarea, -.spoiler-input { - position: relative; -} - -.autosuggest-textarea__textarea, -.spoiler-input__input { - display: block; - box-sizing: border-box; - width: 100%; - margin: 0; - color: $ui-base-color; - background: $simple-background-color; - padding: 10px; - font-family: inherit; - font-size: 14px; - resize: vertical; - border: 0; - outline: 0; - - &:focus { - outline: 0; - } - - @include limited-single-column('screen and (max-width: 600px)') { - font-size: 16px; - } -} - -.spoiler-input__input { - border-radius: 4px; -} - -.autosuggest-textarea__textarea { - min-height: 100px; - border-radius: 4px 4px 0 0; - padding-bottom: 0; - padding-right: 10px + 22px; - resize: none; - - @include limited-single-column('screen and (max-width: 600px)') { - height: 100px !important; // prevent auto-resize textarea - resize: vertical; - } -} - -.autosuggest-textarea__suggestions { - box-sizing: border-box; - display: none; - position: absolute; - top: 100%; - width: 100%; - z-index: 99; - box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4); - background: $ui-secondary-color; - border-radius: 0 0 4px 4px; - color: $ui-base-color; - font-size: 14px; - padding: 6px; - - &.autosuggest-textarea__suggestions--visible { - display: block; - } -} - -.autosuggest-textarea__suggestions__item { - padding: 10px; - cursor: pointer; - border-radius: 4px; - - &:hover, - &:focus, - &:active, - &.selected { - background: darken($ui-secondary-color, 10%); - } -} - -.autosuggest-account, -.autosuggest-emoji { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - line-height: 18px; - font-size: 14px; -} - -.autosuggest-account-icon, -.autosuggest-emoji img { - display: block; - margin-right: 8px; - width: 16px; - height: 16px; -} - -.autosuggest-account .display-name__account { - color: lighten($ui-base-color, 36%); -} - -.character-counter__wrapper { - line-height: 36px; - margin: 0 16px 0 8px; - padding-top: 10px; -} - -.character-counter { - cursor: default; - font-size: 16px; -} - -.character-counter--over { - color: $warning-red; -} - -.getting-started__wrapper { - position: relative; - overflow-y: auto; -} - -.getting-started__footer { - display: flex; - flex-direction: column; -} - -.getting-started { - box-sizing: border-box; - padding-bottom: 235px; - background: url('~images/mastodon-getting-started.png') no-repeat 0 100%; - flex: 1 0 auto; - - p { - color: $ui-secondary-color; - } - - a { - color: $ui-base-lighter-color; - } -} - -.setting-text { - color: $ui-primary-color; - background: transparent; - border: none; - border-bottom: 2px solid $ui-primary-color; - box-sizing: border-box; - display: block; - font-family: inherit; - margin-bottom: 10px; - padding: 7px 0; - width: 100%; - - &:focus, - &:active { - color: $primary-text-color; - border-bottom-color: $ui-highlight-color; - } - - @include limited-single-column('screen and (max-width: 600px)') { - font-size: 16px; - } - - &.light { - color: $ui-base-color; - border-bottom: 2px solid lighten($ui-base-color, 27%); - - &:focus, - &:active { - color: $ui-base-color; - border-bottom-color: $ui-highlight-color; - } - } -} - -@import 'boost'; - -button.icon-button i.fa-retweet { - background-position: 0 0; - height: 19px; - transition: background-position 0.9s steps(10); - transition-duration: 0s; - vertical-align: middle; - width: 22px; - - &::before { - display: none !important; - } -} - -button.icon-button.active i.fa-retweet { - transition-duration: 0.9s; - background-position: 0 100%; -} - -.status-card { - display: flex; - cursor: pointer; - font-size: 14px; - border: 1px solid lighten($ui-base-color, 8%); - border-radius: 4px; - color: $ui-base-lighter-color; - margin-top: 14px; - text-decoration: none; - overflow: hidden; - - &:hover { - background: lighten($ui-base-color, 8%); - } -} - -.status-card-video, -.status-card-rich, -.status-card-photo { - margin-top: 14px; - overflow: hidden; - - iframe { - width: 100%; - height: auto; - } -} - -.status-card-photo { - display: block; - text-decoration: none; - - img { - display: block; - width: 100%; - height: auto; - margin: 0; - } -} - -.status-card-video { - iframe { - width: 100%; - height: 100%; - } -} - -.status-card__title { - display: block; - font-weight: 500; - margin-bottom: 5px; - color: $ui-primary-color; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.status-card__content { - flex: 1 1 auto; - overflow: hidden; - padding: 14px 14px 14px 8px; -} - -.status-card__description { - color: $ui-primary-color; -} - -.status-card__host { - display: block; - margin-top: 5px; - font-size: 13px; -} - -.status-card__image { - flex: 0 0 100px; - background: lighten($ui-base-color, 8%); -} - -.status-card.horizontal { - display: block; - - .status-card__image { - width: 100%; - } - - .status-card__image-image { - border-radius: 4px 4px 0 0; - } -} - -.status-card__image-image { - border-radius: 4px 0 0 4px; - display: block; - height: auto; - margin: 0; - width: 100%; -} - -.load-more { - display: block; - color: $ui-base-lighter-color; - background-color: transparent; - border: 0; - font-size: inherit; - text-align: center; - line-height: inherit; - margin: 0; - padding: 15px; - width: 100%; - clear: both; - - &:hover { - background: lighten($ui-base-color, 2%); - } -} - -.missing-indicator { - text-align: center; - font-size: 16px; - font-weight: 500; - color: lighten($ui-base-color, 16%); - background: $ui-base-color; - cursor: default; - display: flex; - flex: 1 1 auto; - align-items: center; - justify-content: center; - - & > div { - background: url('~images/mastodon-not-found.png') no-repeat center -50px; - padding-top: 210px; - width: 100%; - } -} - -.column-header__wrapper { - position: relative; - flex: 0 0 auto; - - &.active { - &::before { - display: block; - content: ""; - position: absolute; - top: 35px; - left: 0; - right: 0; - margin: 0 auto; - width: 60%; - pointer-events: none; - height: 28px; - z-index: 1; - background: radial-gradient(ellipse, rgba($ui-highlight-color, 0.23) 0%, rgba($ui-highlight-color, 0) 60%); - } - } -} - -.column-header { - display: flex; - padding: 15px; - font-size: 16px; - background: lighten($ui-base-color, 4%); - flex: 0 0 auto; - cursor: pointer; - position: relative; - z-index: 2; - outline: 0; - - &.active { - box-shadow: 0 1px 0 rgba($ui-highlight-color, 0.3); - - .column-header__icon { - color: $ui-highlight-color; - text-shadow: 0 0 10px rgba($ui-highlight-color, 0.4); - } - } - - &:focus, - &:active { - outline: 0; - } -} - -.column-header__buttons { - height: 48px; - display: flex; - margin: -15px; - margin-left: 0; -} - -.column-header__button { - background: lighten($ui-base-color, 4%); - border: 0; - color: $ui-primary-color; - cursor: pointer; - font-size: 16px; - padding: 0 15px; - - &:hover { - color: lighten($ui-primary-color, 7%); - } - - &.active { - color: $primary-text-color; - background: lighten($ui-base-color, 8%); - - &:hover { - color: $primary-text-color; - background: lighten($ui-base-color, 8%); - } - } - - // glitch - added focus ring for keyboard navigation - &:focus { - text-shadow: 0 0 4px darken($ui-highlight-color, 5%); - } -} - -.scrollable > div > :first-child .notification__dismiss-overlay > .wrappy { - border-top: 1px solid $ui-base-color; -} - -.notification__dismiss-overlay { - overflow: hidden; - position: absolute; - top: 0; - right: 0; - bottom: -1px; - padding-left: 15px; // space for the box shadow to be visible - - z-index: 999; - align-items: center; - justify-content: flex-end; - cursor: pointer; - - display: flex; - - .wrappy { - width: $dismiss-overlay-width; - align-self: stretch; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: lighten($ui-base-color, 8%); - border-left: 1px solid lighten($ui-base-color, 20%); - box-shadow: 0 0 5px black; - border-bottom: 1px solid $ui-base-color; - } - - .ckbox { - border: 2px solid $ui-primary-color; - border-radius: 2px; - width: 30px; - height: 30px; - font-size: 20px; - color: $ui-primary-color; - text-shadow: 0 0 5px black; - display: flex; - justify-content: center; - align-items: center; - } - - &:focus { - outline: 0 !important; - - .ckbox { - box-shadow: 0 0 1px 1px $ui-highlight-color; - } - } -} - -.column-header__notif-cleaning-buttons { - display: flex; - align-items: stretch; - justify-content: space-around; - - button { - @extend .column-header__button; - background: transparent; - text-align: center; - padding: 10px 0; - white-space: pre-wrap; - } - - b { - font-weight: bold; - } -} - -// The notifs drawer with no padding to have more space for the buttons -.column-header__collapsible-inner.nopad-drawer { - padding: 0; -} - -.column-header__collapsible { - max-height: 70vh; - overflow: hidden; - overflow-y: auto; - color: $ui-primary-color; - transition: max-height 150ms ease-in-out, opacity 300ms linear; - opacity: 1; - - &.collapsed { - max-height: 0; - opacity: 0.5; - } - - &.animating { - overflow-y: hidden; - } - - // notif cleaning drawer - &.ncd { - transition: none; - &.collapsed { - max-height: 0; - opacity: 0.7; - } - } -} - -.column-header__collapsible-inner { - background: lighten($ui-base-color, 8%); - padding: 15px; -} - -.column-header__setting-btn { - &:hover { - color: lighten($ui-primary-color, 4%); - text-decoration: underline; - } -} - -.column-header__setting-arrows { - float: right; - - .column-header__setting-btn { - padding: 0 10px; - - &:last-child { - padding-right: 0; - } - } -} - -.column-header__title { - display: inline-block; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - flex: 1; -} - -.text-btn { - display: inline-block; - padding: 0; - font-family: inherit; - font-size: inherit; - color: inherit; - border: 0; - background: transparent; - cursor: pointer; -} - -.column-header__icon { - display: inline-block; - margin-right: 5px; -} - -.loading-indicator { - color: lighten($ui-base-color, 26%); - font-size: 12px; - font-weight: 400; - text-transform: uppercase; - overflow: visible; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - span { - display: block; - float: left; - margin-left: 50%; - transform: translateX(-50%); - margin: 82px 0 0 50%; - white-space: nowrap; - animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000); - } -} - -.loading-indicator__figure { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 0; - height: 0; - box-sizing: border-box; - border: 0 solid lighten($ui-base-color, 26%); - border-radius: 50%; - animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000); -} - -@keyframes loader-figure { - 0% { - width: 0; - height: 0; - background-color: lighten($ui-base-color, 26%); - } - - 29% { - background-color: lighten($ui-base-color, 26%); - } - - 30% { - width: 42px; - height: 42px; - background-color: transparent; - border-width: 21px; - opacity: 1; - } - - 100% { - width: 42px; - height: 42px; - border-width: 0; - opacity: 0; - background-color: transparent; - } -} - -@keyframes loader-label { - 0% { opacity: 0.25; } - 30% { opacity: 1; } - 100% { opacity: 0.25; } -} - -.video-error-cover { - align-items: center; - background: $base-overlay-background; - color: $primary-text-color; - cursor: pointer; - display: flex; - flex-direction: column; - height: 100%; - justify-content: center; - margin-top: 8px; - position: relative; - text-align: center; - z-index: 100; -} - -.media-spoiler { - background: $base-overlay-background; - color: $ui-primary-color; - border: 0; - width: 100%; - height: 100%; - justify-content: center; - position: relative; - text-align: center; - z-index: 100; - display: flex; - flex-direction: column; - align-items: stretch; - - .status__content > & { - margin-top: 15px; // Add margin when used bare for NSFW video player - } - - @include fullwidth-gallery; -} - -.media-spoiler__warning { - display: block; - font-size: 14px; -} - -.media-spoiler__trigger { - display: block; - font-size: 11px; - font-weight: 500; -} - -.spoiler-button { - display: none; - left: 4px; - position: absolute; - text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color; - top: 4px; - z-index: 100; - - &.spoiler-button--visible { - display: block; - } -} - -.modal-container--preloader { - background: lighten($ui-base-color, 8%); -} - -.account--panel { - background: lighten($ui-base-color, 4%); - border-top: 1px solid lighten($ui-base-color, 8%); - border-bottom: 1px solid lighten($ui-base-color, 8%); - display: flex; - flex-direction: row; - padding: 10px 0; -} - -.account--panel__button, -.detailed-status__button { - flex: 1 1 auto; - text-align: center; -} - -.column-settings__outer { - background: lighten($ui-base-color, 8%); - padding: 15px; -} - -.column-settings__section { - color: $ui-primary-color; - cursor: default; - display: block; - font-weight: 500; - margin-bottom: 10px; -} - -.column-settings__row { - .text-btn { - margin-bottom: 15px; - } -} - -.modal-container__nav { - align-items: center; - background: rgba($base-overlay-background, 0.5); - box-sizing: border-box; - border: 0; - color: $primary-text-color; - cursor: pointer; - display: flex; - font-size: 24px; - height: 100%; - padding: 30px 15px; - position: absolute; - top: 0; -} - -.modal-container__nav--left { - left: -61px; -} - -.modal-container__nav--right { - right: -61px; -} - -.account--follows-info { - color: $primary-text-color; - position: absolute; - top: 10px; - left: 10px; - opacity: 0.7; - display: inline-block; - vertical-align: top; - background-color: rgba($base-overlay-background, 0.4); - text-transform: uppercase; - font-size: 11px; - font-weight: 500; - padding: 4px; - border-radius: 4px; -} - -.account--action-button { - position: absolute; - top: 10px; - right: 20px; -} - -.setting-toggle { - display: block; - line-height: 24px; -} - -.setting-toggle__label, -.setting-meta__label { - color: $ui-primary-color; - display: inline-block; - margin-bottom: 14px; - margin-left: 8px; - vertical-align: middle; -} - -.setting-meta__label { - color: $ui-primary-color; - float: right; -} - -.empty-column-indicator, -.error-column { - color: lighten($ui-base-color, 20%); - background: $ui-base-color; - text-align: center; - padding: 20px; - font-size: 15px; - font-weight: 400; - cursor: default; - display: flex; - flex: 1 1 auto; - align-items: center; - justify-content: center; - @supports(display: grid) { // hack to fix Chrome <57 - contain: strict; - } - - a { - color: $ui-highlight-color; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } -} - -.error-column { - flex-direction: column; -} - -@keyframes heartbeat { - from { - transform: scale(1); - transform-origin: center center; - animation-timing-function: ease-out; - } - - 10% { - transform: scale(0.91); - animation-timing-function: ease-in; - } - - 17% { - transform: scale(0.98); - animation-timing-function: ease-out; - } - - 33% { - transform: scale(0.87); - animation-timing-function: ease-in; - } - - 45% { - transform: scale(1); - animation-timing-function: ease-out; - } -} - -.pulse-loading { - animation: heartbeat 1.5s ease-in-out infinite both; -} - -.emoji-picker-dropdown__menu { - background: $simple-background-color; - position: absolute; - box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4); - border-radius: 4px; - margin-top: 5px; - - .emoji-mart-scroll { - transition: opacity 200ms ease; - } - - &.selecting .emoji-mart-scroll { - opacity: 0.5; - } -} - -.emoji-picker-dropdown__modifiers { - position: absolute; - top: 60px; - right: 11px; - cursor: pointer; -} - -.emoji-picker-dropdown__modifiers__menu { - position: absolute; - z-index: 4; - top: -4px; - left: -8px; - background: $simple-background-color; - border-radius: 4px; - box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2); - overflow: hidden; - - button { - display: block; - cursor: pointer; - border: 0; - padding: 4px 8px; - background: transparent; - - &:hover, - &:focus, - &:active { - background: rgba($ui-secondary-color, 0.4); - } - } - - .emoji-mart-emoji { - height: 22px; - } -} - -.emoji-mart-emoji { - span { - background-repeat: no-repeat; - } -} - -.upload-area { - align-items: center; - background: rgba($base-overlay-background, 0.8); - display: flex; - height: 100%; - justify-content: center; - left: 0; - opacity: 0; - position: absolute; - top: 0; - visibility: hidden; - width: 100%; - z-index: 2000; - - * { - pointer-events: none; - } -} - -.upload-area__drop { - width: 320px; - height: 160px; - display: flex; - box-sizing: border-box; - position: relative; - padding: 8px; -} - -.upload-area__background { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: -1; - border-radius: 4px; - background: $ui-base-color; - box-shadow: 0 0 5px rgba($base-shadow-color, 0.2); -} - -.upload-area__content { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - color: $ui-secondary-color; - font-size: 18px; - font-weight: 500; - border: 2px dashed $ui-base-lighter-color; - border-radius: 4px; -} - -.upload-progress { - padding: 10px; - color: $ui-base-lighter-color; - overflow: hidden; - display: flex; - - .fa { - font-size: 34px; - margin-right: 10px; - } - - span { - font-size: 12px; - text-transform: uppercase; - font-weight: 500; - display: block; - } -} - -.upload-progess__message { - flex: 1 1 auto; -} - -.upload-progress__backdrop { - width: 100%; - height: 6px; - border-radius: 6px; - background: $ui-base-lighter-color; - position: relative; - margin-top: 5px; -} - -.upload-progress__tracker { - position: absolute; - left: 0; - top: 0; - height: 6px; - background: $ui-highlight-color; - border-radius: 6px; -} - -.emoji-button { - display: block; - font-size: 24px; - line-height: 24px; - margin-left: 2px; - width: 24px; - outline: 0; - cursor: pointer; - - &:active, - &:focus { - outline: 0 !important; - } - - img { - filter: grayscale(100%); - opacity: 0.8; - display: block; - margin: 0; - width: 22px; - height: 22px; - margin-top: 2px; - } - - &:hover, - &:active, - &:focus { - img { - opacity: 1; - filter: none; - } - } -} - -.dropdown--active .emoji-button img { - opacity: 1; - filter: none; -} - -.privacy-dropdown__dropdown { - position: absolute; - background: $simple-background-color; - box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); - border-radius: 4px; - margin-left: 40px; - overflow: hidden; -} - -.privacy-dropdown__option { - color: $ui-base-color; - padding: 10px; - cursor: pointer; - display: flex; - - &:hover, - &.active { - background: $ui-highlight-color; - color: $primary-text-color; - - .privacy-dropdown__option__content { - color: $primary-text-color; - - strong { - color: $primary-text-color; - } - } - } - - &.active:hover { - background: lighten($ui-highlight-color, 4%); - } -} - -.privacy-dropdown__option__icon { - display: flex; - align-items: center; - justify-content: center; - margin-right: 10px; -} - -.privacy-dropdown__option__content { - flex: 1 1 auto; - color: darken($ui-primary-color, 24%); - - strong { - font-weight: 500; - display: block; - color: $ui-base-color; - } -} - -.privacy-dropdown.active { - .privacy-dropdown__value { - background: $simple-background-color; - border-radius: 4px 4px 0 0; - box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); - - .icon-button { - transition: none; - } - - &.active { - background: $ui-highlight-color; - - .icon-button { - color: $primary-text-color; - } - } - } - - .privacy-dropdown__dropdown { - display: block; - box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1); - } -} - -.advanced-options-dropdown { - position: relative; -} - -.advanced-options-dropdown__dropdown { - display: none; - position: absolute; - left: 0; - top: 27px; - width: 210px; - background: $simple-background-color; - border-radius: 0 4px 4px; - z-index: 2; - overflow: hidden; -} - -.advanced-options-dropdown__option { - color: $ui-base-color; - padding: 10px; - cursor: pointer; - display: flex; - - &:hover, - &.active { - background: $ui-highlight-color; - color: $primary-text-color; - - .advanced-options-dropdown__option__content { - color: $primary-text-color; - - strong { - color: $primary-text-color; - } - } - } - - &.active:hover { - background: lighten($ui-highlight-color, 4%); - } -} - -.advanced-options-dropdown__option__toggle { - display: flex; - align-items: center; - justify-content: center; - margin-right: 10px; -} - -.advanced-options-dropdown__option__content { - flex: 1 1 auto; - color: darken($ui-primary-color, 24%); - - strong { - font-weight: 500; - display: block; - color: $ui-base-color; - } -} - -.advanced-options-dropdown.open { - .advanced-options-dropdown__value { - background: $simple-background-color; - border-radius: 4px 4px 0 0; - box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); - } - - .advanced-options-dropdown__dropdown { - display: block; - box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1); - } -} - - -.search { - position: relative; - margin-bottom: 10px; -} - -.search__input { - outline: 0; - box-sizing: border-box; - display: block; - width: 100%; - border: none; - padding: 10px; - padding-right: 30px; - font-family: inherit; - background: $ui-base-color; - color: $ui-primary-color; - font-size: 14px; - margin: 0; - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, - &:focus, - &:active { - outline: 0 !important; - } - - &:focus { - background: lighten($ui-base-color, 4%); - } - - @include limited-single-column('screen and (max-width: 600px)') { - font-size: 16px; - } -} - -.search__icon { - .fa { - position: absolute; - top: 10px; - right: 10px; - z-index: 2; - display: inline-block; - opacity: 0; - transition: all 100ms linear; - font-size: 18px; - width: 18px; - height: 18px; - color: $ui-secondary-color; - cursor: default; - pointer-events: none; - - &.active { - pointer-events: auto; - opacity: 0.3; - } - } - - .fa-search { - transform: rotate(90deg); - - &.active { - pointer-events: none; - transform: rotate(0deg); - } - } - - .fa-times-circle { - top: 11px; - transform: rotate(0deg); - cursor: pointer; - - &.active { - transform: rotate(90deg); - } - - &:hover { - color: $primary-text-color; - } - } -} - -.search-results__header { - color: $ui-base-lighter-color; - background: lighten($ui-base-color, 2%); - border-bottom: 1px solid darken($ui-base-color, 4%); - padding: 15px 10px; - font-size: 14px; - font-weight: 500; -} - -.search-results__section { - background: $ui-base-color; -} - -.search-results__hashtag { - display: block; - padding: 10px; - color: $ui-secondary-color; - text-decoration: none; - - &:hover, - &:active, - &:focus { - color: lighten($ui-secondary-color, 4%); - text-decoration: underline; - } -} - -.modal-root { - transition: opacity 0.3s linear; - will-change: opacity; - z-index: 9999; -} - -.modal-root__overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba($base-overlay-background, 0.7); -} - -.modal-root__container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - align-content: space-around; - z-index: 9999; - pointer-events: none; - user-select: none; -} - -.modal-root__modal { - pointer-events: auto; - display: flex; - z-index: 9999; -} - -.media-modal { - max-width: 80vw; - max-height: 80vh; - position: relative; - - .extended-video-player, - img, - canvas, - video { - max-width: 80vw; - max-height: 80vh; - width: auto; - height: auto; - margin: auto; - } - - .extended-video-player, - video { - display: flex; - width: 80vw; - height: 80vh; - } - - img, - canvas { - display: block; - background: url('~images/void.png') repeat; - object-fit: contain; - } - - .react-swipeable-view-container { - max-width: 80vw; - } -} - -.media-modal__content { - background: $base-overlay-background; -} - -.media-modal__pagination { - width: 100%; - text-align: center; - position: absolute; - left: 0; - bottom: -40px; -} - -.media-modal__page-dot { - display: inline-block; -} - -.media-modal__button { - background-color: $white; - height: 12px; - width: 12px; - border-radius: 6px; - margin: 10px; - padding: 0; - border: 0; - font-size: 0; -} - -.media-modal__button--active { - background-color: $ui-highlight-color; -} - -.media-modal__close { - position: absolute; - right: 4px; - top: 4px; - z-index: 100; -} - -.onboarding-modal, -.error-modal, -.embed-modal { - background: $ui-secondary-color; - color: $ui-base-color; - border-radius: 8px; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.onboarding-modal__pager { - height: 80vh; - width: 80vw; - max-width: 520px; - max-height: 420px; - - .react-swipeable-view-container > div { - width: 100%; - height: 100%; - box-sizing: border-box; - padding: 25px; - display: none; - flex-direction: column; - align-items: center; - justify-content: center; - display: flex; - user-select: text; - } -} - -.error-modal__body { - height: 80vh; - width: 80vw; - max-width: 520px; - max-height: 420px; - position: relative; - - & > div { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - box-sizing: border-box; - padding: 25px; - display: none; - flex-direction: column; - align-items: center; - justify-content: center; - display: flex; - opacity: 0; - user-select: text; - } -} - -.error-modal__body { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; -} - -@media screen and (max-width: 550px) { - .onboarding-modal { - width: 100%; - height: 100%; - border-radius: 0; - } - - .onboarding-modal__pager { - width: 100%; - height: auto; - max-width: none; - max-height: none; - flex: 1 1 auto; - } -} - -.onboarding-modal__paginator, -.error-modal__footer { - flex: 0 0 auto; - background: darken($ui-secondary-color, 8%); - display: flex; - padding: 25px; - - & > div { - min-width: 33px; - } - - .onboarding-modal__nav, - .error-modal__nav { - color: darken($ui-secondary-color, 34%); - background-color: transparent; - border: 0; - font-size: 14px; - font-weight: 500; - padding: 0; - line-height: inherit; - height: auto; - - &:hover, - &:focus, - &:active { - color: darken($ui-secondary-color, 38%); - } - - &.onboarding-modal__done, - &.onboarding-modal__next { - color: $ui-highlight-color; - } - } -} - -.error-modal__footer { - justify-content: center; -} - -.onboarding-modal__dots { - flex: 1 1 auto; - display: flex; - align-items: center; - justify-content: center; -} - -.onboarding-modal__dot { - width: 14px; - height: 14px; - border-radius: 14px; - background: darken($ui-secondary-color, 16%); - margin: 0 3px; - cursor: pointer; - - &:hover { - background: darken($ui-secondary-color, 18%); - } - - &.active { - cursor: default; - background: darken($ui-secondary-color, 24%); - } -} - -.onboarding-modal__page__wrapper { - pointer-events: none; - - &.onboarding-modal__page__wrapper--active { - pointer-events: auto; - } -} - -.onboarding-modal__page { - cursor: default; - line-height: 21px; - - h1 { - font-size: 18px; - font-weight: 500; - color: $ui-base-color; - margin-bottom: 20px; - } - - a { - color: $ui-highlight-color; - - &:hover, - &:focus, - &:active { - color: lighten($ui-highlight-color, 4%); - } - } - - p { - font-size: 16px; - color: lighten($ui-base-color, 8%); - margin-top: 10px; - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - - strong { - font-weight: 500; - background: $ui-base-color; - color: $ui-secondary-color; - border-radius: 4px; - font-size: 14px; - padding: 3px 6px; - } - } -} - -.onboarding-modal__page-one { - display: flex; - align-items: center; -} - -.onboarding-modal__page-one__elephant-friend { - background: url('~images/elephant-friend-1.png') no-repeat center center / contain; - width: 155px; - height: 193px; - margin-right: 15px; -} - -@media screen and (max-width: 400px) { - .onboarding-modal__page-one { - flex-direction: column; - align-items: normal; - } - - .onboarding-modal__page-one__elephant-friend { - width: 100%; - height: 30vh; - max-height: 160px; - margin-bottom: 5vh; - } -} - -.onboarding-modal__page-two, -.onboarding-modal__page-three, -.onboarding-modal__page-four, -.onboarding-modal__page-five { - p { - text-align: left; - } - - .figure { - background: darken($ui-base-color, 8%); - color: $ui-secondary-color; - margin-bottom: 20px; - border-radius: 4px; - padding: 10px; - text-align: center; - font-size: 14px; - box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.3); - - .onboarding-modal__image { - border-radius: 4px; - margin-bottom: 10px; - } - - &.non-interactive { - pointer-events: none; - text-align: left; - } - } -} - -.onboarding-modal__page-four__columns { - .row { - display: flex; - margin-bottom: 20px; - - & > div { - flex: 1 1 0; - margin: 0 10px; - - &:first-child { - margin-left: 0; - } - - &:last-child { - margin-right: 0; - } - - p { - text-align: center; - } - } - - &:last-child { - margin-bottom: 0; - } - } - - .column-header { - color: $primary-text-color; - } -} - -@media screen and (max-width: 320px) and (max-height: 600px) { - .onboarding-modal__page p { - font-size: 14px; - line-height: 20px; - } - - .onboarding-modal__page-two .figure, - .onboarding-modal__page-three .figure, - .onboarding-modal__page-four .figure, - .onboarding-modal__page-five .figure { - font-size: 12px; - margin-bottom: 10px; - } - - .onboarding-modal__page-four__columns .row { - margin-bottom: 10px; - } - - .onboarding-modal__page-four__columns .column-header { - padding: 5px; - font-size: 12px; - } -} - -.onboarding-modal__image { - border-radius: 8px; - width: 70vw; - max-width: 450px; - max-height: auto; - display: block; - margin: auto; - margin-bottom: 20px; -} - -.onboard-sliders { - display: inline-block; - max-width: 30px; - max-height: auto; - margin-left: 10px; -} - -.boost-modal, -.confirmation-modal, -.report-modal, -.actions-modal, -.mute-modal { - background: lighten($ui-secondary-color, 8%); - color: $ui-base-color; - border-radius: 8px; - overflow: hidden; - max-width: 90vw; - width: 480px; - position: relative; - flex-direction: column; - - .status__display-name { - display: flex; - } -} - -.actions-modal { - .status { - background: $white; - border-bottom-color: $ui-secondary-color; - padding-top: 10px; - padding-bottom: 10px; - } - - .dropdown-menu__separator { - border-bottom-color: $ui-secondary-color; - } -} - -.boost-modal__container { - overflow-x: scroll; - padding: 10px; - - .status { - user-select: text; - border-bottom: 0; - } -} - -.boost-modal__action-bar, -.confirmation-modal__action-bar, -.mute-modal__action-bar, -.report-modal__action-bar { - display: flex; - justify-content: space-between; - background: $ui-secondary-color; - padding: 10px; - line-height: 36px; - - & > div { - flex: 1 1 auto; - text-align: right; - color: lighten($ui-base-color, 33%); - padding-right: 10px; - } - - .button { - flex: 0 0 auto; - } -} - -.boost-modal__status-header { - font-size: 15px; -} - -.boost-modal__status-time { - float: right; - font-size: 14px; -} - -.confirmation-modal { - max-width: 85vw; - - @media screen and (min-width: 480px) { - max-width: 380px; - } -} - -.mute-modal { - line-height: 24px; -} - -.mute-modal .react-toggle { - vertical-align: middle; -} - -.report-modal__statuses, -.report-modal__comment { - padding: 10px; -} - -.report-modal__statuses { - min-height: 20vh; - max-height: 40vh; - overflow-y: auto; - overflow-x: hidden; -} - -.report-modal__comment { - .setting-text { - margin-top: 10px; - } -} - -.actions-modal { - .status { - overflow-y: auto; - max-height: 300px; - } - - max-height: 80vh; - max-width: 80vw; - - .actions-modal__item-label { - font-weight: 500; - } - - ul { - overflow-y: auto; - flex-shrink: 0; - - li:empty { - margin: 0; - } - - li:not(:empty) { - a { - color: $ui-base-color; - display: flex; - padding: 12px 16px; - font-size: 15px; - align-items: center; - text-decoration: none; - - &, - button { - transition: none; - } - - &.active, - &:hover, - &:active, - &:focus { - &, - button { - background: $ui-highlight-color; - color: $primary-text-color; - } - } - - button:first-child { - margin-right: 10px; - } - } - } - } -} - -.confirmation-modal__action-bar, -.mute-modal__action-bar { - .confirmation-modal__cancel-button, - .mute-modal__cancel-button { - background-color: transparent; - color: darken($ui-secondary-color, 34%); - font-size: 14px; - font-weight: 500; - - &:hover, - &:focus, - &:active { - color: darken($ui-secondary-color, 38%); - } - } -} - -.confirmation-modal__container, -.mute-modal__container, -.report-modal__target { - padding: 30px; - font-size: 16px; - text-align: center; - - strong { - font-weight: 500; - } -} - -.loading-bar { - background-color: $ui-highlight-color; - height: 3px; - position: absolute; - top: 0; - left: 0; -} - -.media-gallery__gifv__label { - display: block; - position: absolute; - color: $primary-text-color; - background: rgba($base-overlay-background, 0.5); - bottom: 6px; - left: 6px; - padding: 2px 6px; - border-radius: 2px; - font-size: 11px; - font-weight: 600; - z-index: 1; - pointer-events: none; - opacity: 0.9; - transition: opacity 0.1s ease; -} - -.media-gallery__gifv { - &.autoplay { - .media-gallery__gifv__label { - display: none; - } - } - - &:hover { - .media-gallery__gifv__label { - opacity: 1; - } - } -} - -.attachment-list { - display: flex; - font-size: 14px; - border: 1px solid lighten($ui-base-color, 8%); - border-radius: 4px; - margin-top: 14px; - overflow: hidden; -} - -.attachment-list__icon { - flex: 0 0 auto; - color: $ui-base-lighter-color; - padding: 8px 18px; - cursor: default; - border-right: 1px solid lighten($ui-base-color, 8%); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: 26px; - - .fa { - display: block; - } -} - -.attachment-list__list { - list-style: none; - padding: 4px 0; - padding-left: 8px; - display: flex; - flex-direction: column; - justify-content: center; - - li { - display: block; - padding: 4px 0; - } - - a { - text-decoration: none; - color: $ui-base-lighter-color; - font-weight: 500; - - &:hover { - text-decoration: underline; - } - } -} - -/* Media Gallery */ -.media-gallery { - box-sizing: border-box; - margin-top: 15px; - overflow: hidden; - position: relative; - background: $base-shadow-color; - width: 100%; - height: 110px; - - .detailed-status & { - margin-left: -12px; - width: calc(100% + 24px); - height: 250px; - } - - @include fullwidth-gallery; -} - -.media-gallery__item { - border: none; - box-sizing: border-box; - display: block; - float: left; - position: relative; - - &.standalone { - .media-gallery__item-gifv-thumbnail { - transform: none; - } - } -} - -.media-gallery__item-thumbnail { - cursor: zoom-in; - text-decoration: none; - width: 100%; - height: 100%; - line-height: 0; - display: flex; - - img { - width: 100%; - object-fit: contain; - - &:not(.letterbox) { - height: 100%; - object-fit: cover; - } - } -} - -.media-gallery__gifv { - height: 100%; - overflow: hidden; - position: relative; - width: 100%; - display: flex; - justify-content: center; -} - -.media-gallery__item-gifv-thumbnail { - cursor: zoom-in; - height: 100%; - position: relative; - z-index: 1; - object-fit: contain; - - &:not(.letterbox) { - height: 100%; - object-fit: cover; - } -} - -.media-gallery__item-thumbnail-label { - clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ - clip: rect(1px, 1px, 1px, 1px); - overflow: hidden; - position: absolute; -} -/* End Media Gallery */ - -/* Status Video Player */ -.status__video-player { - display: flex; - align-items: center; - background: $base-shadow-color; - box-sizing: border-box; - cursor: default; /* May not be needed */ - margin-top: 15px; - overflow: hidden; - position: relative; - width: 100%; - - @include fullwidth-gallery; -} - -.status__video-player-video { - position: relative; - width: 100%; - z-index: 1; - - &:not(.letterbox) { - height: 100%; - object-fit: cover; - } -} - -.status__video-player-expand, -.status__video-player-mute { - color: $primary-text-color; - opacity: 0.8; - position: absolute; - right: 4px; - text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color; -} - -.status__video-player-spoiler { - display: none; - color: $primary-text-color; - left: 4px; - position: absolute; - text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color; - top: 4px; - z-index: 100; - - &.status__video-player-spoiler--visible { - display: block; - } -} - -.status__video-player-expand { - bottom: 4px; - z-index: 100; -} - -.status__video-player-mute { - top: 4px; - z-index: 5; -} - -.video-player { - overflow: hidden; - position: relative; - background: $base-shadow-color; - width: 100%; - max-width: 100%; - height: 110px; - - .detailed-status & { - margin-left: -12px; - width: calc(100% + 24px); - height: 250px; - } - - @include fullwidth-gallery; - - video { - height: 100%; - width: 100%; - z-index: 1; - } - - &.fullscreen { - width: 100% !important; - height: 100% !important; - margin: 0; - - video { - max-width: 100% !important; - max-height: 100% !important; - } - } - - &.inline { - video { - object-fit: cover; - position: relative; - top: 50%; - transform: translateY(-50%); - } - } - - &__controls { - position: absolute; - z-index: 2; - bottom: 0; - left: 0; - right: 0; - box-sizing: border-box; - background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent); - padding: 0 10px; - opacity: 0; - transition: opacity .1s ease; - - &.active { - opacity: 1; - } - } - - &.inactive { - video, - .video-player__controls { - visibility: hidden; - } - } - - &__spoiler { - display: none; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 4; - border: 0; - background: $base-shadow-color; - color: $ui-primary-color; - transition: none; - pointer-events: none; - - &.active { - display: block; - pointer-events: auto; - - &:hover, - &:active, - &:focus { - color: lighten($ui-primary-color, 8%); - } - } - - &__title { - display: block; - font-size: 14px; - } - - &__subtitle { - display: block; - font-size: 11px; - font-weight: 500; - } - } - - &__buttons { - padding-bottom: 10px; - font-size: 16px; - - &.left { - float: left; - - button { - padding-right: 10px; - } - } - - &.right { - float: right; - - button { - padding-left: 10px; - } - } - - button { - background: transparent; - padding: 0; - border: 0; - color: $white; - - &:active, - &:hover, - &:focus { - color: $ui-highlight-color; - } - } - } - - &__seek { - cursor: pointer; - height: 24px; - position: relative; - - &::before { - content: ""; - width: 100%; - background: rgba($white, 0.35); - display: block; - position: absolute; - height: 4px; - top: 10px; - } - - &__progress, - &__buffer { - display: block; - position: absolute; - height: 4px; - top: 10px; - background: $ui-highlight-color; - } - - &__buffer { - background: rgba($white, 0.2); - } - - &__handle { - position: absolute; - z-index: 3; - opacity: 0; - border-radius: 50%; - width: 12px; - height: 12px; - top: 6px; - margin-left: -6px; - transition: opacity .1s ease; - background: $ui-highlight-color; - pointer-events: none; - - &.active { - opacity: 1; - } - } - - &:hover { - .video-player__seek__handle { - opacity: 1; - } - } - } -} - -.media-spoiler-video { - background-size: cover; - background-repeat: no-repeat; - background-position: center; - cursor: pointer; - margin-top: 15px; - position: relative; - width: 100%; - - @include fullwidth-gallery; - - border: 0; - display: block; -} - -.media-spoiler-video-play-icon { - border-radius: 100px; - color: rgba($primary-text-color, 0.8); - font-size: 36px; - left: 50%; - padding: 5px; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); -} -/* End Video Player */ - -.account-gallery__container { - margin: -2px; - padding: 4px; - display: flex; - flex-wrap: wrap; -} - -.account-gallery__item { - flex: 1 1 auto; - width: calc(100% / 3 - 4px); - height: 95px; - margin: 2px; - - a { - display: block; - width: 100%; - height: 100%; - background-color: $base-overlay-background; - background-size: cover; - background-position: center; - position: relative; - color: inherit; - text-decoration: none; - - &:hover, - &:active, - &:focus { - outline: 0; - } - } -} - -.account-section-headline { - color: $ui-base-lighter-color; - background: lighten($ui-base-color, 2%); - border-bottom: 1px solid lighten($ui-base-color, 4%); - padding: 15px 10px; - font-size: 14px; - font-weight: 500; - position: relative; - cursor: default; - - &::before, - &::after { - display: block; - content: ""; - position: absolute; - bottom: 0; - left: 18px; - width: 0; - height: 0; - border-style: solid; - border-width: 0 10px 10px; - border-color: transparent transparent lighten($ui-base-color, 4%); - } - - &::after { - bottom: -1px; - border-color: transparent transparent $ui-base-color; - } -} - -::-webkit-scrollbar-thumb { - border-radius: 0; -} - -.search-popout { - background: $simple-background-color; - border-radius: 4px; - padding: 10px 14px; - padding-bottom: 14px; - margin-top: 10px; - color: $ui-primary-color; - box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); - - h4 { - text-transform: uppercase; - color: $ui-primary-color; - font-size: 13px; - font-weight: 500; - margin-bottom: 10px; - } - - li { - padding: 4px 0; - } - - ul { - margin-bottom: 10px; - } - - em { - font-weight: 500; - color: $ui-base-color; - } -} - -noscript { - text-align: center; - - img { - width: 200px; - opacity: 0.5; - animation: flicker 4s infinite; - } - - div { - font-size: 14px; - margin: 30px auto; - color: $ui-secondary-color; - max-width: 400px; - - a { - color: $ui-highlight-color; - text-decoration: underline; - - &:hover { - text-decoration: none; - } - } - } -} - -@keyframes flicker { - 0% { opacity: 1; } - 30% { opacity: 0.75; } - 100% { opacity: 1; } -} - -@media screen and (max-width: 630px) and (max-height: 400px) { - $duration: 400ms; - $delay: 100ms; - - .tabs-bar, - .search { - will-change: margin-top; - transition: margin-top $duration $delay; - } - - .navigation-bar { - will-change: padding-bottom; - transition: padding-bottom $duration $delay; - } - - .navigation-bar { - & > a:first-child { - will-change: margin-top, margin-left, width; - transition: margin-top $duration $delay, margin-left $duration ($duration + $delay); - } - - & > .navigation-bar__profile-edit { - will-change: margin-top; - transition: margin-top $duration $delay; - } - - & > .icon-button { - will-change: opacity; - transition: opacity $duration $delay; - } - } - - .is-composing { - .tabs-bar, - .search { - margin-top: -50px; - } - - .navigation-bar { - padding-bottom: 0; - - & > a:first-child { - margin-top: -50px; - margin-left: -40px; - } - - .navigation-bar__profile { - padding-top: 2px; - } - - .navigation-bar__profile-edit { - position: absolute; - margin-top: -50px; - } - - .icon-button { - pointer-events: auto; - opacity: 1; - } - } - } - - // fixes for the navbar-under mode - .is-composing.navbar-under { - .search { - margin-top: -20px; - margin-bottom: -20px; - .search__icon { - display: none; - } - } - } -} - -// more fixes for the navbar-under mode -@mixin fix-margins-for-navbar-under { - .tabs-bar { - margin-top: 0 !important; - margin-bottom: -6px !important; - } -} - -.single-column.navbar-under { - @include fix-margins-for-navbar-under; -} - -.auto-columns.navbar-under { - @media screen and (max-width: 360px) { - @include fix-margins-for-navbar-under; - } -} - -.auto-columns.navbar-under .react-swipeable-view-container .columns-area, -.single-column.navbar-under .react-swipeable-view-container .columns-area { - @media screen and (max-width: 360px) { - height: 100% !important; - } -} - -.embed-modal { - max-width: 80vw; - max-height: 80vh; - - h4 { - padding: 30px; - font-weight: 500; - font-size: 16px; - text-align: center; - } - - .embed-modal__container { - padding: 10px; - - .hint { - margin-bottom: 15px; - } - - .embed-modal__html { - color: $ui-secondary-color; - outline: 0; - box-sizing: border-box; - display: block; - width: 100%; - border: none; - padding: 10px; - font-family: 'mastodon-font-monospace', monospace; - background: $ui-base-color; - color: $ui-primary-color; - font-size: 14px; - margin: 0; - margin-bottom: 15px; - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, - &:focus, - &:active { - outline: 0 !important; - } - - &:focus { - background: lighten($ui-base-color, 4%); - } - - @media screen and (max-width: 600px) { - font-size: 16px; - } - } - - .embed-modal__iframe { - width: 400px; - max-width: 100%; - overflow: hidden; - border: 0; - } - } -} - -@import 'doodle'; diff --git a/app/javascript/themes/glitch/styles/containers.scss b/app/javascript/themes/glitch/styles/containers.scss deleted file mode 100644 index af2589e23..000000000 --- a/app/javascript/themes/glitch/styles/containers.scss +++ /dev/null @@ -1,116 +0,0 @@ -.container { - width: 700px; - margin: 0 auto; - margin-top: 40px; - - @media screen and (max-width: 740px) { - width: 100%; - margin: 0; - } -} - -.logo-container { - margin: 100px auto; - margin-bottom: 50px; - - @media screen and (max-width: 400px) { - margin: 30px auto; - margin-bottom: 20px; - } - - h1 { - display: flex; - justify-content: center; - align-items: center; - - img { - height: 42px; - margin-right: 10px; - } - - a { - display: flex; - justify-content: center; - align-items: center; - color: $primary-text-color; - text-decoration: none; - outline: 0; - padding: 12px 16px; - line-height: 32px; - font-family: 'mastodon-font-display', sans-serif; - font-weight: 500; - font-size: 14px; - } - } -} - -.compose-standalone { - .compose-form { - width: 400px; - margin: 0 auto; - padding: 20px 0; - margin-top: 40px; - box-sizing: border-box; - - @media screen and (max-width: 400px) { - width: 100%; - margin-top: 0; - padding: 20px; - } - } -} - -.account-header { - width: 400px; - margin: 0 auto; - display: flex; - font-size: 13px; - line-height: 18px; - box-sizing: border-box; - padding: 20px 0; - padding-bottom: 0; - margin-bottom: -30px; - margin-top: 40px; - - @media screen and (max-width: 440px) { - width: 100%; - margin: 0; - margin-bottom: 10px; - padding: 20px; - padding-bottom: 0; - } - - .avatar { - width: 40px; - height: 40px; - margin-right: 8px; - - img { - width: 100%; - height: 100%; - display: block; - margin: 0; - border-radius: 4px; - } - } - - .name { - flex: 1 1 auto; - color: $ui-secondary-color; - width: calc(100% - 88px); - - .username { - display: block; - font-weight: 500; - text-overflow: ellipsis; - overflow: hidden; - } - } - - .logout-link { - display: block; - font-size: 32px; - line-height: 40px; - margin-left: 8px; - } -} diff --git a/app/javascript/themes/glitch/styles/doodle.scss b/app/javascript/themes/glitch/styles/doodle.scss deleted file mode 100644 index a4a1cfc84..000000000 --- a/app/javascript/themes/glitch/styles/doodle.scss +++ /dev/null @@ -1,86 +0,0 @@ -$doodleBg: #d9e1e8; -.doodle-modal { - @extend .boost-modal; - width: unset; -} - -.doodle-modal__container { - background: $doodleBg; - text-align: center; - line-height: 0; // remove weird gap under canvas - canvas { - border: 5px solid $doodleBg; - } -} - -.doodle-modal__action-bar { - @extend .boost-modal__action-bar; - - .filler { - flex-grow: 1; - margin: 0; - padding: 0; - } - - .doodle-toolbar { - line-height: 1; - - display: flex; - flex-direction: column; - flex-grow: 0; - justify-content: space-around; - - &.with-inputs { - label { - display: inline-block; - width: 70px; - text-align: right; - margin-right: 2px; - } - - input[type="number"],input[type="text"] { - width: 40px; - } - span.val { - display: inline-block; - text-align: left; - width: 50px; - } - } - } - - .doodle-palette { - padding-right: 0 !important; - border: 1px solid black; - line-height: .2rem; - flex-grow: 0; - background: white; - - button { - appearance: none; - width: 1rem; - height: 1rem; - margin: 0; padding: 0; - text-align: center; - color: black; - text-shadow: 0 0 1px white; - cursor: pointer; - box-shadow: inset 0 0 1px rgba(white, .5); - border: 1px solid black; - outline-offset:-1px; - - &.foreground { - outline: 1px dashed white; - } - - &.background { - outline: 1px dashed red; - } - - &.foreground.background { - outline: 1px dashed red; - border-color: white; - } - } - } -} diff --git a/app/javascript/themes/glitch/styles/emoji_picker.scss b/app/javascript/themes/glitch/styles/emoji_picker.scss deleted file mode 100644 index 2b46d30fc..000000000 --- a/app/javascript/themes/glitch/styles/emoji_picker.scss +++ /dev/null @@ -1,199 +0,0 @@ -.emoji-mart { - &, - * { - box-sizing: border-box; - line-height: 1.15; - } - - font-size: 13px; - display: inline-block; - color: $ui-base-color; - - .emoji-mart-emoji { - padding: 6px; - } -} - -.emoji-mart-bar { - border: 0 solid darken($ui-secondary-color, 8%); - - &:first-child { - border-bottom-width: 1px; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - background: $ui-secondary-color; - } - - &:last-child { - border-top-width: 1px; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; - display: none; - } -} - -.emoji-mart-anchors { - display: flex; - justify-content: space-between; - padding: 0 6px; - color: $ui-primary-color; - line-height: 0; -} - -.emoji-mart-anchor { - position: relative; - flex: 1; - text-align: center; - padding: 12px 4px; - overflow: hidden; - transition: color .1s ease-out; - cursor: pointer; - - &:hover { - color: darken($ui-primary-color, 4%); - } -} - -.emoji-mart-anchor-selected { - color: darken($ui-highlight-color, 3%); - - &:hover { - color: darken($ui-highlight-color, 3%); - } - - .emoji-mart-anchor-bar { - bottom: 0; - } -} - -.emoji-mart-anchor-bar { - position: absolute; - bottom: -3px; - left: 0; - width: 100%; - height: 3px; - background-color: darken($ui-highlight-color, 3%); -} - -.emoji-mart-anchors { - i { - display: inline-block; - width: 100%; - max-width: 22px; - } - - svg { - fill: currentColor; - max-height: 18px; - } -} - -.emoji-mart-scroll { - overflow-y: scroll; - height: 270px; - max-height: 35vh; - padding: 0 6px 6px; - background: $simple-background-color; - will-change: transform; -} - -.emoji-mart-search { - padding: 10px; - padding-right: 45px; - background: $simple-background-color; - - input { - font-size: 14px; - font-weight: 400; - padding: 7px 9px; - font-family: inherit; - display: block; - width: 100%; - background: rgba($ui-secondary-color, 0.3); - color: $ui-primary-color; - border: 1px solid $ui-secondary-color; - border-radius: 4px; - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, - &:focus, - &:active { - outline: 0 !important; - } - } -} - -.emoji-mart-category .emoji-mart-emoji { - cursor: pointer; - - span { - z-index: 1; - position: relative; - text-align: center; - } - - &:hover::before { - z-index: 0; - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba($ui-secondary-color, 0.7); - border-radius: 100%; - } -} - -.emoji-mart-category-label { - z-index: 2; - position: relative; - position: -webkit-sticky; - position: sticky; - top: 0; - - span { - display: block; - width: 100%; - font-weight: 500; - padding: 5px 6px; - background: $simple-background-color; - } -} - -.emoji-mart-emoji { - position: relative; - display: inline-block; - font-size: 0; - - span { - width: 22px; - height: 22px; - } -} - -.emoji-mart-no-results { - font-size: 14px; - text-align: center; - padding-top: 70px; - color: $ui-primary-color; - - .emoji-mart-category-label { - display: none; - } - - .emoji-mart-no-results-label { - margin-top: .2em; - } - - .emoji-mart-emoji:hover::before { - content: none; - } -} - -.emoji-mart-preview { - display: none; -} diff --git a/app/javascript/themes/glitch/styles/footer.scss b/app/javascript/themes/glitch/styles/footer.scss deleted file mode 100644 index 2d953b34e..000000000 --- a/app/javascript/themes/glitch/styles/footer.scss +++ /dev/null @@ -1,30 +0,0 @@ -.footer { - text-align: center; - margin-top: 30px; - font-size: 12px; - color: darken($ui-secondary-color, 25%); - - .domain { - font-weight: 500; - - a { - color: inherit; - text-decoration: none; - } - } - - .powered-by, - .single-user-login { - font-weight: 400; - - a { - color: inherit; - text-decoration: underline; - font-weight: 500; - - &:hover { - text-decoration: none; - } - } - } -} diff --git a/app/javascript/themes/glitch/styles/forms.scss b/app/javascript/themes/glitch/styles/forms.scss deleted file mode 100644 index 61fcf286f..000000000 --- a/app/javascript/themes/glitch/styles/forms.scss +++ /dev/null @@ -1,540 +0,0 @@ -code { - font-family: 'mastodon-font-monospace', monospace; - font-weight: 400; -} - -.form-container { - max-width: 400px; - padding: 20px; - margin: 0 auto; -} - -.simple_form { - .input { - margin-bottom: 15px; - overflow: hidden; - } - - span.hint { - display: block; - color: $ui-primary-color; - font-size: 12px; - margin-top: 4px; - } - - h4 { - text-transform: uppercase; - font-size: 13px; - font-weight: 500; - color: $ui-primary-color; - padding-bottom: 8px; - margin-bottom: 8px; - border-bottom: 1px solid lighten($ui-base-color, 8%); - } - - p.hint { - margin-bottom: 15px; - color: $ui-primary-color; - - &.subtle-hint { - text-align: center; - font-size: 12px; - line-height: 18px; - margin-top: 15px; - margin-bottom: 0; - color: $ui-primary-color; - - a { - color: $ui-highlight-color; - } - } - } - - .card { - margin-bottom: 15px; - } - - strong { - font-weight: 500; - } - - .label_input { - display: flex; - - label { - flex: 0 0 auto; - } - - input { - flex: 1 1 auto; - } - } - - .input.with_label { - padding: 15px 0; - margin-bottom: 0; - - .label_input { - flex-wrap: wrap; - align-items: flex-start; - } - - &.select .label_input { - align-items: initial; - } - - .label_input > label { - font-family: inherit; - font-size: 16px; - color: $primary-text-color; - display: block; - padding-top: 5px; - margin-bottom: 5px; - flex: 1; - min-width: 150px; - word-wrap: break-word; - - &.select { - flex: 0; - } - - & ~ * { - margin-left: 10px; - } - } - - ul { - flex: 390px; - } - - &.boolean { - padding: initial; - margin-bottom: initial; - - .label_input > label { - font-family: inherit; - font-size: 14px; - color: $primary-text-color; - display: block; - width: auto; - } - - label.checkbox { - position: relative; - padding-left: 25px; - flex: 1 1 auto; - } - } - } - - .input.with_block_label { - & > label { - font-family: inherit; - font-size: 16px; - color: $primary-text-color; - display: block; - padding-top: 5px; - } - - .hint { - margin-bottom: 15px; - } - - li { - float: left; - width: 50%; - } - } - - .fields-group { - margin-bottom: 25px; - } - - .input.radio_buttons .radio label { - margin-bottom: 5px; - font-family: inherit; - font-size: 14px; - color: $primary-text-color; - display: block; - width: auto; - } - - .input.boolean { - margin-bottom: 5px; - - label { - font-family: inherit; - font-size: 14px; - color: $primary-text-color; - display: block; - width: auto; - } - - label.checkbox { - position: relative; - padding-left: 25px; - flex: 1 1 auto; - } - - input[type=checkbox] { - position: absolute; - left: 0; - top: 5px; - margin: 0; - } - - .hint { - padding-left: 25px; - margin-left: 0; - } - } - - .check_boxes { - .checkbox { - label { - font-family: inherit; - font-size: 14px; - color: $primary-text-color; - display: block; - width: auto; - position: relative; - padding-top: 5px; - padding-left: 25px; - flex: 1 1 auto; - } - - input[type=checkbox] { - position: absolute; - left: 0; - top: 5px; - margin: 0; - } - } - } - - input[type=text], - input[type=number], - input[type=email], - input[type=password], - textarea { - background: transparent; - box-sizing: border-box; - border: 0; - border-bottom: 2px solid $ui-primary-color; - border-radius: 2px 2px 0 0; - padding: 7px 4px; - font-size: 16px; - color: $primary-text-color; - display: block; - width: 100%; - outline: 0; - font-family: inherit; - resize: vertical; - - &:invalid { - box-shadow: none; - } - - &:focus:invalid { - border-bottom-color: $error-value-color; - } - - &:required:valid { - border-bottom-color: $valid-value-color; - } - - &:active, - &:focus { - border-bottom-color: $ui-highlight-color; - background: rgba($base-overlay-background, 0.1); - } - } - - .input.field_with_errors { - label { - color: $error-value-color; - } - - input[type=text], - input[type=email], - input[type=password] { - border-bottom-color: $error-value-color; - } - - .error { - display: block; - font-weight: 500; - color: $error-value-color; - margin-top: 4px; - } - } - - .actions { - margin-top: 30px; - display: flex; - } - - button, - .button, - .block-button { - display: block; - width: 100%; - border: 0; - border-radius: 4px; - background: $ui-highlight-color; - color: $primary-text-color; - font-size: 18px; - line-height: inherit; - height: auto; - padding: 10px; - text-transform: uppercase; - text-decoration: none; - text-align: center; - box-sizing: border-box; - cursor: pointer; - font-weight: 500; - outline: 0; - margin-bottom: 10px; - margin-right: 10px; - - &:last-child { - margin-right: 0; - } - - &:hover { - background-color: lighten($ui-highlight-color, 5%); - } - - &:active, - &:focus { - background-color: darken($ui-highlight-color, 5%); - } - - &.negative { - background: $error-value-color; - - &:hover { - background-color: lighten($error-value-color, 5%); - } - - &:active, - &:focus { - background-color: darken($error-value-color, 5%); - } - } - } - - select { - font-size: 16px; - max-height: 29px; - } - - .input-with-append { - position: relative; - - .input input { - padding-right: 127px; - } - - .append { - position: absolute; - right: 0; - top: 0; - padding: 7px 4px; - padding-bottom: 9px; - font-size: 16px; - color: $ui-base-lighter-color; - font-family: inherit; - pointer-events: none; - cursor: default; - } - } -} - -.flash-message { - background: lighten($ui-base-color, 8%); - color: $ui-primary-color; - border-radius: 4px; - padding: 15px 10px; - margin-bottom: 30px; - box-shadow: 0 0 5px rgba($base-shadow-color, 0.2); - text-align: center; - - p { - margin-bottom: 15px; - } - - .oauth-code { - color: $ui-secondary-color; - outline: 0; - box-sizing: border-box; - display: block; - width: 100%; - border: none; - padding: 10px; - font-family: 'mastodon-font-monospace', monospace; - background: $ui-base-color; - color: $ui-primary-color; - font-size: 14px; - margin: 0; - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, - &:focus, - &:active { - outline: 0 !important; - } - - &:focus { - background: lighten($ui-base-color, 4%); - } - } - - strong { - font-weight: 500; - } - - @media screen and (max-width: 740px) and (min-width: 441px) { - margin-top: 40px; - } -} - -.form-footer { - margin-top: 30px; - text-align: center; - - a { - color: $ui-primary-color; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } -} - -.oauth-prompt, -.follow-prompt { - margin-bottom: 30px; - text-align: center; - color: $ui-primary-color; - - h2 { - font-size: 16px; - margin-bottom: 30px; - } - - strong { - color: $ui-secondary-color; - font-weight: 500; - } - - @media screen and (max-width: 740px) and (min-width: 441px) { - margin-top: 40px; - } -} - -.qr-wrapper { - display: flex; - flex-wrap: wrap; - align-items: flex-start; -} - -.qr-code { - flex: 0 0 auto; - background: $simple-background-color; - padding: 4px; - margin: 0 10px 20px 0; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - display: inline-block; - - svg { - display: block; - margin: 0; - } -} - -.qr-alternative { - margin-bottom: 20px; - color: $ui-secondary-color; - flex: 150px; - - samp { - display: block; - font-size: 14px; - } -} - -.table-form { - p { - margin-bottom: 15px; - - strong { - font-weight: 500; - } - } -} - -.simple_form, -.table-form { - .warning { - box-sizing: border-box; - background: rgba($error-value-color, 0.5); - color: $primary-text-color; - text-shadow: 1px 1px 0 rgba($base-shadow-color, 0.3); - box-shadow: 0 2px 6px rgba($base-shadow-color, 0.4); - border-radius: 4px; - padding: 10px; - margin-bottom: 15px; - - a { - color: $primary-text-color; - text-decoration: underline; - - &:hover, - &:focus, - &:active { - text-decoration: none; - } - } - - strong { - font-weight: 600; - display: block; - margin-bottom: 5px; - - .fa { - font-weight: 400; - } - } - } -} - -.action-pagination { - display: flex; - flex-wrap: wrap; - align-items: center; - - .actions, - .pagination { - flex: 1 1 auto; - } - - .actions { - padding: 30px 0; - padding-right: 20px; - flex: 0 0 auto; - } -} - -.post-follow-actions { - text-align: center; - color: $ui-primary-color; - - div { - margin-bottom: 4px; - } -} diff --git a/app/javascript/themes/glitch/styles/index.scss b/app/javascript/themes/glitch/styles/index.scss deleted file mode 100644 index c962a1f62..000000000 --- a/app/javascript/themes/glitch/styles/index.scss +++ /dev/null @@ -1,22 +0,0 @@ -@import 'mixins'; -@import 'variables'; -@import 'styles/fonts/roboto'; -@import 'styles/fonts/roboto-mono'; -@import 'styles/fonts/montserrat'; - -@import 'reset'; -@import 'basics'; -@import 'containers'; -@import 'lists'; -@import 'footer'; -@import 'compact_header'; -@import 'landing_strip'; -@import 'forms'; -@import 'accounts'; -@import 'stream_entries'; -@import 'components'; -@import 'emoji_picker'; -@import 'about'; -@import 'tables'; -@import 'admin'; -@import 'rtl'; diff --git a/app/javascript/themes/glitch/styles/landing_strip.scss b/app/javascript/themes/glitch/styles/landing_strip.scss deleted file mode 100644 index 0bf9daafd..000000000 --- a/app/javascript/themes/glitch/styles/landing_strip.scss +++ /dev/null @@ -1,36 +0,0 @@ -.landing-strip, -.memoriam-strip { - background: rgba(darken($ui-base-color, 7%), 0.8); - color: $ui-primary-color; - font-weight: 400; - padding: 14px; - border-radius: 4px; - margin-bottom: 20px; - display: flex; - align-items: center; - - strong, - a { - font-weight: 500; - } - - a { - color: inherit; - text-decoration: underline; - } - - .logo { - width: 30px; - height: 30px; - flex: 0 0 auto; - margin-right: 15px; - } - - @media screen and (max-width: 740px) { - margin-bottom: 0; - } -} - -.memoriam-strip { - background: rgba($base-shadow-color, 0.7); -} diff --git a/app/javascript/themes/glitch/styles/lists.scss b/app/javascript/themes/glitch/styles/lists.scss deleted file mode 100644 index 6019cd800..000000000 --- a/app/javascript/themes/glitch/styles/lists.scss +++ /dev/null @@ -1,19 +0,0 @@ -.no-list { - list-style: none; - - li { - display: inline-block; - margin: 0 5px; - } -} - -.recovery-codes { - list-style: none; - margin: 0 auto; - - li { - font-size: 125%; - line-height: 1.5; - letter-spacing: 1px; - } -} diff --git a/app/javascript/themes/glitch/styles/reset copy.scss b/app/javascript/themes/glitch/styles/reset copy.scss deleted file mode 100644 index cc5ba9d7c..000000000 --- a/app/javascript/themes/glitch/styles/reset copy.scss +++ /dev/null @@ -1,91 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} - -/* HTML5 display-role reset for older browsers */ -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display: block; -} - -body { - line-height: 1; -} - -ol, ul { - list-style: none; -} - -blockquote, q { - quotes: none; -} - -blockquote:before, blockquote:after, -q:before, q:after { - content: ''; - content: none; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-thumb { - background: lighten($ui-base-color, 4%); - border: 0px none $base-border-color; - border-radius: 50px; -} - -::-webkit-scrollbar-thumb:hover { - background: lighten($ui-base-color, 6%); -} - -::-webkit-scrollbar-thumb:active { - background: lighten($ui-base-color, 4%); -} - -::-webkit-scrollbar-track { - border: 0px none $base-border-color; - border-radius: 0; - background: rgba($base-overlay-background, 0.1); -} - -::-webkit-scrollbar-track:hover { - background: $ui-base-color; -} - -::-webkit-scrollbar-track:active { - background: $ui-base-color; -} - -::-webkit-scrollbar-corner { - background: transparent; -} diff --git a/app/javascript/themes/glitch/styles/reset.scss b/app/javascript/themes/glitch/styles/reset.scss deleted file mode 100644 index cc5ba9d7c..000000000 --- a/app/javascript/themes/glitch/styles/reset.scss +++ /dev/null @@ -1,91 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} - -/* HTML5 display-role reset for older browsers */ -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display: block; -} - -body { - line-height: 1; -} - -ol, ul { - list-style: none; -} - -blockquote, q { - quotes: none; -} - -blockquote:before, blockquote:after, -q:before, q:after { - content: ''; - content: none; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-thumb { - background: lighten($ui-base-color, 4%); - border: 0px none $base-border-color; - border-radius: 50px; -} - -::-webkit-scrollbar-thumb:hover { - background: lighten($ui-base-color, 6%); -} - -::-webkit-scrollbar-thumb:active { - background: lighten($ui-base-color, 4%); -} - -::-webkit-scrollbar-track { - border: 0px none $base-border-color; - border-radius: 0; - background: rgba($base-overlay-background, 0.1); -} - -::-webkit-scrollbar-track:hover { - background: $ui-base-color; -} - -::-webkit-scrollbar-track:active { - background: $ui-base-color; -} - -::-webkit-scrollbar-corner { - background: transparent; -} diff --git a/app/javascript/themes/glitch/styles/rtl.scss b/app/javascript/themes/glitch/styles/rtl.scss deleted file mode 100644 index 67bfa8a38..000000000 --- a/app/javascript/themes/glitch/styles/rtl.scss +++ /dev/null @@ -1,254 +0,0 @@ -body.rtl { - direction: rtl; - - .column-link__icon, - .column-header__icon { - margin-right: 0; - margin-left: 5px; - } - - .character-counter__wrapper { - margin-right: 8px; - margin-left: 16px; - } - - .navigation-bar__profile { - margin-left: 0; - margin-right: 8px; - } - - .search__input { - padding-right: 10px; - padding-left: 30px; - } - - .search__icon .fa { - right: auto; - left: 10px; - } - - .column-header__buttons { - left: 0; - right: auto; - } - - .column-header__back-button { - padding-left: 5px; - padding-right: 0; - } - - .column-header__setting-arrows { - float: left; - } - - .compose-form__modifiers { - border-radius: 0 0 0 4px; - } - - .setting-toggle { - margin-left: 0; - margin-right: 8px; - } - - .setting-meta__label { - float: left; - } - - .status__avatar { - left: auto; - right: 10px; - } - - .status, - .activity-stream .status.light { - padding-left: 10px; - padding-right: 68px; - } - - .status__info .status__display-name, - .activity-stream .status.light .status__display-name { - padding-left: 25px; - padding-right: 0; - } - - .activity-stream .pre-header { - padding-right: 68px; - padding-left: 0; - } - - .status__prepend { - margin-left: 0; - margin-right: 68px; - } - - .status__prepend-icon-wrapper { - left: auto; - right: -26px; - } - - .activity-stream .pre-header .pre-header__icon { - left: auto; - right: 42px; - } - - .account__avatar-overlay-overlay { - right: auto; - left: 0; - } - - .column-back-button--slim-button { - right: auto; - left: 0; - } - - .status__relative-time, - .activity-stream .status.light .status__header .status__meta { - float: left; - } - - .activity-stream .detailed-status.light .detailed-status__display-name > div { - float: right; - margin-right: 0; - margin-left: 10px; - } - - .activity-stream .detailed-status.light .detailed-status__meta span > span { - margin-left: 0; - margin-right: 6px; - } - - .status__action-bar-button { - float: right; - margin-right: 0; - margin-left: 18px; - } - - .status__action-bar-dropdown { - float: right; - } - - .privacy-dropdown__dropdown { - margin-left: 0; - margin-right: 40px; - } - - .privacy-dropdown__option__icon { - margin-left: 10px; - margin-right: 0; - } - - .detailed-status__display-avatar { - margin-right: 0; - margin-left: 10px; - float: right; - } - - .detailed-status__favorites, - .detailed-status__reblogs { - margin-left: 0; - margin-right: 6px; - } - - .fa-ul { - margin-left: 0; - margin-left: 2.14285714em; - } - - .fa-li { - left: auto; - right: -2.14285714em; - } - - .admin-wrapper .sidebar ul a i.fa, - a.table-action-link i.fa { - margin-right: 0; - margin-left: 5px; - } - - .simple_form .check_boxes .checkbox label, - .simple_form .input.with_label.boolean label.checkbox { - padding-left: 0; - padding-right: 25px; - } - - .simple_form .check_boxes .checkbox input[type="checkbox"], - .simple_form .input.boolean input[type="checkbox"] { - left: auto; - right: 0; - } - - .simple_form .input-with-append .input input { - padding-left: 127px; - padding-right: 0; - } - - .simple_form .input-with-append .append { - right: auto; - left: 0; - } - - .table th, - .table td { - text-align: right; - } - - .filters .filter-subset { - margin-right: 0; - margin-left: 45px; - } - - .landing-page .header-wrapper .mascot { - right: 60px; - left: auto; - } - - .landing-page .header .hero .floats .float-1 { - left: -120px; - right: auto; - } - - .landing-page .header .hero .floats .float-2 { - left: 210px; - right: auto; - } - - .landing-page .header .hero .floats .float-3 { - left: 110px; - right: auto; - } - - .landing-page .header .links .brand img { - left: 0; - } - - .landing-page .fa-external-link { - padding-right: 5px; - padding-left: 0 !important; - } - - .landing-page .features #mastodon-timeline { - margin-right: 0; - margin-left: 30px; - } - - @media screen and (min-width: 631px) { - .column, - .drawer { - padding-left: 5px; - padding-right: 5px; - - &:first-child { - padding-left: 5px; - padding-right: 10px; - } - } - - .columns-area > div { - .column, - .drawer { - padding-left: 5px; - padding-right: 5px; - } - } - } -} diff --git a/app/javascript/themes/glitch/styles/stream_entries.scss b/app/javascript/themes/glitch/styles/stream_entries.scss deleted file mode 100644 index 453070b7c..000000000 --- a/app/javascript/themes/glitch/styles/stream_entries.scss +++ /dev/null @@ -1,335 +0,0 @@ -.activity-stream { - clear: both; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - - .entry { - background: $simple-background-color; - - .detailed-status.light, - .status.light { - border-bottom: 1px solid $ui-secondary-color; - animation: none; - } - - &:last-child { - &, - .detailed-status.light, - .status.light { - border-bottom: 0; - border-radius: 0 0 4px 4px; - } - } - - &:first-child { - &, - .detailed-status.light, - .status.light { - border-radius: 4px 4px 0 0; - } - - &:last-child { - &, - .detailed-status.light, - .status.light { - border-radius: 4px; - } - } - } - - @media screen and (max-width: 740px) { - &, - .detailed-status.light, - .status.light { - border-radius: 0 !important; - } - } - } - - &.with-header { - .entry { - &:first-child { - &, - .detailed-status.light, - .status.light { - border-radius: 0; - } - - &:last-child { - &, - .detailed-status.light, - .status.light { - border-radius: 0 0 4px 4px; - } - } - } - } - } - - .status.light { - padding: 14px 14px 14px (48px + 14px * 2); - position: relative; - min-height: 48px; - cursor: default; - - .status__header { - font-size: 15px; - - .status__meta { - float: right; - font-size: 14px; - - .status__relative-time { - color: $ui-primary-color; - } - } - } - - .status__display-name { - display: block; - max-width: 100%; - padding-right: 25px; - color: $ui-base-color; - } - - .status__avatar { - position: absolute; - @include avatar-size(48px); - margin-left: -62px; - - & > div { - @include avatar-size(48px); - } - - img { - @include avatar-radius(); - display: block; - } - } - - .display-name { - display: block; - max-width: 100%; - //overflow: hidden; - //white-space: nowrap; - //text-overflow: ellipsis; - - strong { - font-weight: 500; - color: $ui-base-color; - } - - span { - font-size: 14px; - color: $ui-primary-color; - } - } - - .status__content { - color: $ui-base-color; - - a { - color: $ui-highlight-color; - } - - a.status__content__spoiler-link { - color: $primary-text-color; - background: $ui-primary-color; - - &:hover { - background: lighten($ui-primary-color, 8%); - } - } - } - } - - .detailed-status.light { - padding: 14px; - background: $simple-background-color; - cursor: default; - - .detailed-status__display-name { - display: block; - overflow: hidden; - margin-bottom: 15px; - - & > div { - float: left; - margin-right: 10px; - } - - .display-name { - display: block; - max-width: 100%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - - strong { - font-weight: 500; - color: $ui-base-color; - } - - span { - font-size: 14px; - color: $ui-primary-color; - } - } - } - - .avatar { - @include avatar-size(48px); - - img { - @include avatar-radius(); - display: block; - } - } - - .status__content { - color: $ui-base-color; - - a { - color: $ui-highlight-color; - } - - a.status__content__spoiler-link { - color: $primary-text-color; - background: $ui-primary-color; - - &:hover { - background: lighten($ui-primary-color, 8%); - } - } - } - - .detailed-status__meta { - margin-top: 15px; - color: $ui-primary-color; - font-size: 14px; - line-height: 18px; - - a { - color: inherit; - } - - span > span { - font-weight: 500; - font-size: 12px; - margin-left: 6px; - display: inline-block; - } - } - - .status-card { - border-color: lighten($ui-secondary-color, 4%); - color: darken($ui-primary-color, 4%); - - &:hover { - background: lighten($ui-secondary-color, 4%); - } - } - - .status-card__title, - .status-card__description { - color: $ui-base-color; - } - - .status-card__image { - background: $ui-secondary-color; - } - } - - .media-spoiler { - background: $ui-primary-color; - color: $white; - transition: all 100ms linear; - - &:hover, - &:active, - &:focus { - background: darken($ui-primary-color, 5%); - color: unset; - } - } - - .pre-header { - padding: 14px 0; - padding-left: (48px + 14px * 2); - padding-bottom: 0; - margin-bottom: -4px; - color: $ui-primary-color; - font-size: 14px; - position: relative; - - .pre-header__icon { - position: absolute; - left: (48px + 14px * 2 - 30px); - } - - .status__display-name.muted strong { - color: $ui-primary-color; - } - } - - .open-in-web-link { - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } -} - -.embed { - .activity-stream { - box-shadow: none; - - .entry { - - .detailed-status.light { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-items: flex-start; - - .detailed-status__display-name { - flex: 1; - margin: 0 5px 15px 0; - } - - .button.button-secondary.logo-button { - flex: 0 auto; - font-size: 14px; - - svg { - width: 20px; - height: auto; - vertical-align: middle; - margin-right: 5px; - - path:first-child { - fill: $ui-primary-color; - } - - path:last-child { - fill: $simple-background-color; - } - } - - &:active, - &:focus, - &:hover { - svg path:first-child { - fill: lighten($ui-primary-color, 4%); - } - } - } - - .status__content, - .detailed-status__meta { - flex: 100%; - } - } - } - } -} diff --git a/app/javascript/themes/glitch/styles/tables.scss b/app/javascript/themes/glitch/styles/tables.scss deleted file mode 100644 index ad46f5f9f..000000000 --- a/app/javascript/themes/glitch/styles/tables.scss +++ /dev/null @@ -1,76 +0,0 @@ -.table { - width: 100%; - max-width: 100%; - border-spacing: 0; - border-collapse: collapse; - - th, - td { - padding: 8px; - line-height: 18px; - vertical-align: top; - border-top: 1px solid $ui-base-color; - text-align: left; - } - - & > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid $ui-base-color; - border-top: 0; - font-weight: 500; - } - - & > tbody > tr > th { - font-weight: 500; - } - - & > tbody > tr:nth-child(odd) > td, - & > tbody > tr:nth-child(odd) > th { - background: $ui-base-color; - } - - a { - color: $ui-highlight-color; - text-decoration: underline; - - &:hover { - text-decoration: none; - } - } - - strong { - font-weight: 500; - } - - &.inline-table > tbody > tr:nth-child(odd) > td, - &.inline-table > tbody > tr:nth-child(odd) > th { - background: transparent; - } -} - -.table-wrapper { - overflow: auto; - margin-bottom: 20px; -} - -samp { - font-family: 'mastodon-font-monospace', monospace; -} - -a.table-action-link { - text-decoration: none; - display: inline-block; - margin-right: 5px; - padding: 0 10px; - color: rgba($primary-text-color, 0.7); - font-weight: 500; - - &:hover { - color: $primary-text-color; - } - - i.fa { - font-weight: 400; - margin-right: 5px; - } -} diff --git a/app/javascript/themes/glitch/styles/variables.scss b/app/javascript/themes/glitch/styles/variables.scss deleted file mode 100644 index f42d9c8c5..000000000 --- a/app/javascript/themes/glitch/styles/variables.scss +++ /dev/null @@ -1,35 +0,0 @@ -// Commonly used web colors -$black: #000000; // Black -$white: #ffffff; // White -$success-green: #79bd9a; // Padua -$error-red: #df405a; // Cerise -$warning-red: #ff5050; // Sunset Orange -$gold-star: #ca8f04; // 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 - -// Variables for defaults in UI -$base-shadow-color: $black !default; -$base-overlay-background: $black !default; -$base-border-color: $white !default; -$simple-background-color: $white !default; -$primary-text-color: $white !default; -$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-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-highlight-color: $classic-highlight-color !default; // Vibrant - -// Avatar border size (8% default, 100% for rounded avatars) -$ui-avatar-border-size: 8%; - -// More variables -$dismiss-overlay-width: 4rem; diff --git a/app/javascript/themes/glitch/theme.yml b/app/javascript/themes/glitch/theme.yml deleted file mode 100644 index 49fba8f40..000000000 --- a/app/javascript/themes/glitch/theme.yml +++ /dev/null @@ -1,18 +0,0 @@ -# (REQUIRED) The location of the pack file inside `pack_directory`. -pack: index.js - -# (OPTIONAL) The directory which contains the pack file. -# Defaults to the theme directory (`app/javascript/themes/[theme]`), -# but in the case of the vanilla Mastodon theme the pack file is -# somewhere else. -# pack_directory: app/javascript/packs - -# (OPTIONAL) Additional javascript resources to preload, for use with -# lazy-loaded components. It is **STRONGLY RECOMMENDED** that you -# derive these pathnames from `themes/[your-theme]` to ensure that -# they stay unique. (Of course, vanilla doesn't do this ^^;;) -preload: -- themes/glitch/async/getting_started -- themes/glitch/async/compose -- themes/glitch/async/home_timeline -- themes/glitch/async/notifications diff --git a/app/javascript/themes/glitch/util/api.js b/app/javascript/themes/glitch/util/api.js deleted file mode 100644 index ecc703c0a..000000000 --- a/app/javascript/themes/glitch/util/api.js +++ /dev/null @@ -1,26 +0,0 @@ -import axios from 'axios'; -import LinkHeader from './link_header'; - -export const getLinks = response => { - const value = response.headers.link; - - if (!value) { - return { refs: [] }; - } - - return LinkHeader.parse(value); -}; - -export default getState => axios.create({ - headers: { - 'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`, - }, - - transformResponse: [function (data) { - try { - return JSON.parse(data); - } catch(Exception) { - return data; - } - }], -}); diff --git a/app/javascript/themes/glitch/util/async-components.js b/app/javascript/themes/glitch/util/async-components.js deleted file mode 100644 index 91e85fed5..000000000 --- a/app/javascript/themes/glitch/util/async-components.js +++ /dev/null @@ -1,115 +0,0 @@ -export function EmojiPicker () { - return import(/* webpackChunkName: "themes/glitch/async/emoji_picker" */'themes/glitch/util/emoji/emoji_picker'); -} - -export function Compose () { - return import(/* webpackChunkName: "themes/glitch/async/compose" */'themes/glitch/features/compose'); -} - -export function Notifications () { - return import(/* webpackChunkName: "themes/glitch/async/notifications" */'themes/glitch/features/notifications'); -} - -export function HomeTimeline () { - return import(/* webpackChunkName: "themes/glitch/async/home_timeline" */'themes/glitch/features/home_timeline'); -} - -export function PublicTimeline () { - return import(/* webpackChunkName: "themes/glitch/async/public_timeline" */'themes/glitch/features/public_timeline'); -} - -export function CommunityTimeline () { - return import(/* webpackChunkName: "themes/glitch/async/community_timeline" */'themes/glitch/features/community_timeline'); -} - -export function HashtagTimeline () { - return import(/* webpackChunkName: "themes/glitch/async/hashtag_timeline" */'themes/glitch/features/hashtag_timeline'); -} - -export function DirectTimeline() { - return import(/* webpackChunkName: "themes/glitch/async/direct_timeline" */'themes/glitch/features/direct_timeline'); -} - -export function Status () { - return import(/* webpackChunkName: "themes/glitch/async/status" */'themes/glitch/features/status'); -} - -export function GettingStarted () { - return import(/* webpackChunkName: "themes/glitch/async/getting_started" */'themes/glitch/features/getting_started'); -} - -export function PinnedStatuses () { - return import(/* webpackChunkName: "themes/glitch/async/pinned_statuses" */'themes/glitch/features/pinned_statuses'); -} - -export function AccountTimeline () { - return import(/* webpackChunkName: "themes/glitch/async/account_timeline" */'themes/glitch/features/account_timeline'); -} - -export function AccountGallery () { - return import(/* webpackChunkName: "themes/glitch/async/account_gallery" */'themes/glitch/features/account_gallery'); -} - -export function Followers () { - return import(/* webpackChunkName: "themes/glitch/async/followers" */'themes/glitch/features/followers'); -} - -export function Following () { - return import(/* webpackChunkName: "themes/glitch/async/following" */'themes/glitch/features/following'); -} - -export function Reblogs () { - return import(/* webpackChunkName: "themes/glitch/async/reblogs" */'themes/glitch/features/reblogs'); -} - -export function Favourites () { - return import(/* webpackChunkName: "themes/glitch/async/favourites" */'themes/glitch/features/favourites'); -} - -export function FollowRequests () { - return import(/* webpackChunkName: "themes/glitch/async/follow_requests" */'themes/glitch/features/follow_requests'); -} - -export function GenericNotFound () { - return import(/* webpackChunkName: "themes/glitch/async/generic_not_found" */'themes/glitch/features/generic_not_found'); -} - -export function FavouritedStatuses () { - return import(/* webpackChunkName: "themes/glitch/async/favourited_statuses" */'themes/glitch/features/favourited_statuses'); -} - -export function Blocks () { - return import(/* webpackChunkName: "themes/glitch/async/blocks" */'themes/glitch/features/blocks'); -} - -export function Mutes () { - return import(/* webpackChunkName: "themes/glitch/async/mutes" */'themes/glitch/features/mutes'); -} - -export function OnboardingModal () { - return import(/* webpackChunkName: "themes/glitch/async/onboarding_modal" */'themes/glitch/features/ui/components/onboarding_modal'); -} - -export function MuteModal () { - return import(/* webpackChunkName: "themes/glitch/async/mute_modal" */'themes/glitch/features/ui/components/mute_modal'); -} - -export function ReportModal () { - return import(/* webpackChunkName: "themes/glitch/async/report_modal" */'themes/glitch/features/ui/components/report_modal'); -} - -export function SettingsModal () { - return import(/* webpackChunkName: "themes/glitch/async/settings_modal" */'themes/glitch/features/local_settings'); -} - -export function MediaGallery () { - return import(/* webpackChunkName: "themes/glitch/async/media_gallery" */'themes/glitch/components/media_gallery'); -} - -export function Video () { - return import(/* webpackChunkName: "themes/glitch/async/video" */'themes/glitch/features/video'); -} - -export function EmbedModal () { - return import(/* webpackChunkName: "themes/glitch/async/embed_modal" */'themes/glitch/features/ui/components/embed_modal'); -} diff --git a/app/javascript/themes/glitch/util/base_polyfills.js b/app/javascript/themes/glitch/util/base_polyfills.js deleted file mode 100644 index 7856b26f9..000000000 --- a/app/javascript/themes/glitch/util/base_polyfills.js +++ /dev/null @@ -1,18 +0,0 @@ -import 'intl'; -import 'intl/locale-data/jsonp/en'; -import 'es6-symbol/implement'; -import includes from 'array-includes'; -import assign from 'object-assign'; -import isNaN from 'is-nan'; - -if (!Array.prototype.includes) { - includes.shim(); -} - -if (!Object.assign) { - Object.assign = assign; -} - -if (!Number.isNaN) { - Number.isNaN = isNaN; -} diff --git a/app/javascript/themes/glitch/util/bio_metadata.js b/app/javascript/themes/glitch/util/bio_metadata.js deleted file mode 100644 index 599ec20e2..000000000 --- a/app/javascript/themes/glitch/util/bio_metadata.js +++ /dev/null @@ -1,331 +0,0 @@ -/* - -`util/bio_metadata` -=================== - -> For more information on the contents of this file, please contact: -> -> - kibigo! [@kibi@glitch.social] - -This file provides two functions for dealing with bio metadata. The -functions are: - - - __`processBio(content)` :__ - Processes `content` to extract any frontmatter. The returned - object has two properties: `text`, which contains the text of - `content` sans-frontmatter, and `metadata`, which is an array - of key-value pairs (in two-element array format). If no - frontmatter was provided in `content`, then `metadata` will be - an empty array. - - - __`createBio(note, data)` :__ - Reverses the process in `processBio()`; takes a `note` and an - array of two-element arrays (which should give keys and values) - and outputs a string containing a well-formed bio with - frontmatter. - -*/ - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/*********************************************************************\ - - To my lovely code maintainers, - - The syntax recognized by the Mastodon frontend for its bio metadata - feature is a subset of that provided by the YAML 1.2 specification. - In particular, Mastodon recognizes metadata which is provided as an - implicit YAML map, where each key-value pair takes up only a single - line (no multi-line values are permitted). To simplify the level of - processing required, Mastodon metadata frontmatter has been limited - to only allow those characters in the `c-printable` set, as defined - by the YAML 1.2 specification, instead of permitting those from the - `nb-json` characters inside double-quoted strings like YAML proper. - ¶ It is important to note that Mastodon only borrows the *syntax* - of YAML, not its semantics. This is to say, Mastodon won't make any - attempt to interpret the data it receives. `true` will not become a - boolean; `56` will not be interpreted as a number. Rather, each key - and every value will be read as a string, and as a string they will - remain. The order of the pairs is unchanged, and any duplicate keys - are preserved. However, YAML escape sequences will be replaced with - the proper interpretations according to the YAML 1.2 specification. - ¶ The implementation provided below interprets `<br>` as `\n` and - allows for an open <p> tag at the beginning of the bio. It replaces - the escaped character entities `'` and `"` with single or - double quotes, respectively, prior to processing. However, no other - escaped characters are replaced, not even those which might have an - impact on the syntax otherwise. These minor allowances are provided - because the Mastodon backend will insert these things automatically - into a bio before sending it through the API, so it is important we - account for them. Aside from this, the YAML frontmatter must be the - very first thing in the bio, leading with three consecutive hyphen- - minues (`---`), and ending with the same or, alternatively, instead - with three periods (`...`). No limits have been set with respect to - the number of characters permitted in the frontmatter, although one - should note that only limited space is provided for them in the UI. - ¶ The regular expression used to check the existence of, and then - process, the YAML frontmatter has been split into a number of small - components in the code below, in the vain hope that it will be much - easier to read and to maintain. I leave it to the future readers of - this code to determine the extent of my successes in this endeavor. - - UPDATE 19 Oct 2017: We no longer allow character escapes inside our - double-quoted strings for ease of processing. We now internally use - the name "ƔAML" in our code to clarify that this is Not Quite YAML. - - Sending love + warmth eternal, - - kibigo [@kibi@glitch.social] - -\*********************************************************************/ - -/* "u" FLAG COMPATABILITY */ - -let compat_mode = false; -try { - new RegExp('.', 'u'); -} catch (e) { - compat_mode = true; -} - -/* CONVENIENCE FUNCTIONS */ - -const unirex = str => compat_mode ? new RegExp(str) : new RegExp(str, 'u'); -const rexstr = exp => '(?:' + exp.source + ')'; - -/* CHARACTER CLASSES */ - -const DOCUMENT_START = /^/; -const DOCUMENT_END = /$/; -const ALLOWED_CHAR = unirex( // `c-printable` in the YAML 1.2 spec. - compat_mode ? '[\t\n\r\x20-\x7e\x85\xa0-\ufffd]' : '[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]' - ); -const WHITE_SPACE = /[ \t]/; -const LINE_BREAK = /\r?\n|\r|<br\s*\/?>/; -const INDICATOR = /[-?:,[\]{}&#*!|>'"%@`]/; -const FLOW_CHAR = /[,[\]{}]/; - -/* NEGATED CHARACTER CLASSES */ - -const NOT_WHITE_SPACE = unirex('(?!' + rexstr(WHITE_SPACE) + ')[^]'); -const NOT_LINE_BREAK = unirex('(?!' + rexstr(LINE_BREAK) + ')[^]'); -const NOT_INDICATOR = unirex('(?!' + rexstr(INDICATOR) + ')[^]'); -const NOT_FLOW_CHAR = unirex('(?!' + rexstr(FLOW_CHAR) + ')[^]'); -const NOT_ALLOWED_CHAR = unirex( - '(?!' + rexstr(ALLOWED_CHAR) + ')[^]' -); - -/* BASIC CONSTRUCTS */ - -const ANY_WHITE_SPACE = unirex(rexstr(WHITE_SPACE) + '*'); -const ANY_ALLOWED_CHARS = unirex(rexstr(ALLOWED_CHAR) + '*'); -const NEW_LINE = unirex( - rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) -); -const SOME_NEW_LINES = unirex( - '(?:' + rexstr(NEW_LINE) + ')+' -); -const POSSIBLE_STARTS = unirex( - rexstr(DOCUMENT_START) + rexstr(/<p[^<>]*>/) + '?' -); -const POSSIBLE_ENDS = unirex( - rexstr(SOME_NEW_LINES) + '|' + - rexstr(DOCUMENT_END) + '|' + - rexstr(/<\/p>/) -); -const QUOTE_CHAR = unirex( - '(?=' + rexstr(NOT_LINE_BREAK) + ')[^"]' -); -const ANY_QUOTE_CHAR = unirex( - rexstr(QUOTE_CHAR) + '*' -); - -const ESCAPED_APOS = unirex( - '(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/) -); -const ANY_ESCAPED_APOS = unirex( - rexstr(ESCAPED_APOS) + '*' -); -const FIRST_KEY_CHAR = unirex( - '(?=' + rexstr(NOT_LINE_BREAK) + ')' + - '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + - rexstr(NOT_INDICATOR) + '|' + - rexstr(/[?:-]/) + - '(?=' + rexstr(NOT_LINE_BREAK) + ')' + - '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + - '(?=' + rexstr(NOT_FLOW_CHAR) + ')' -); -const FIRST_VALUE_CHAR = unirex( - '(?=' + rexstr(NOT_LINE_BREAK) + ')' + - '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + - rexstr(NOT_INDICATOR) + '|' + - rexstr(/[?:-]/) + - '(?=' + rexstr(NOT_LINE_BREAK) + ')' + - '(?=' + rexstr(NOT_WHITE_SPACE) + ')' - // Flow indicators are allowed in values. -); -const LATER_KEY_CHAR = unirex( - rexstr(WHITE_SPACE) + '|' + - '(?=' + rexstr(NOT_LINE_BREAK) + ')' + - '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + - '(?=' + rexstr(NOT_FLOW_CHAR) + ')' + - rexstr(/[^:#]#?/) + '|' + - rexstr(/:/) + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' -); -const LATER_VALUE_CHAR = unirex( - rexstr(WHITE_SPACE) + '|' + - '(?=' + rexstr(NOT_LINE_BREAK) + ')' + - '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + - // Flow indicators are allowed in values. - rexstr(/[^:#]#?/) + '|' + - rexstr(/:/) + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' -); - -/* YAML CONSTRUCTS */ - -const ƔAML_START = unirex( - rexstr(ANY_WHITE_SPACE) + '---' -); -const ƔAML_END = unirex( - rexstr(ANY_WHITE_SPACE) + '(?:---|\.\.\.)' -); -const ƔAML_LOOKAHEAD = unirex( - '(?=' + - rexstr(ƔAML_START) + - rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) + - rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) + - ')' -); -const ƔAML_DOUBLE_QUOTE = unirex( - '"' + rexstr(ANY_QUOTE_CHAR) + '"' -); -const ƔAML_SINGLE_QUOTE = unirex( - '\'' + rexstr(ANY_ESCAPED_APOS) + '\'' -); -const ƔAML_SIMPLE_KEY = unirex( - rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*' -); -const ƔAML_SIMPLE_VALUE = unirex( - rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*' -); -const ƔAML_KEY = unirex( - rexstr(ƔAML_DOUBLE_QUOTE) + '|' + - rexstr(ƔAML_SINGLE_QUOTE) + '|' + - rexstr(ƔAML_SIMPLE_KEY) -); -const ƔAML_VALUE = unirex( - rexstr(ƔAML_DOUBLE_QUOTE) + '|' + - rexstr(ƔAML_SINGLE_QUOTE) + '|' + - rexstr(ƔAML_SIMPLE_VALUE) -); -const ƔAML_SEPARATOR = unirex( - rexstr(ANY_WHITE_SPACE) + - ':' + rexstr(WHITE_SPACE) + - rexstr(ANY_WHITE_SPACE) -); -const ƔAML_LINE = unirex( - '(' + rexstr(ƔAML_KEY) + ')' + - rexstr(ƔAML_SEPARATOR) + - '(' + rexstr(ƔAML_VALUE) + ')' -); - -/* FRONTMATTER REGEX */ - -const ƔAML_FRONTMATTER = unirex( - rexstr(POSSIBLE_STARTS) + - rexstr(ƔAML_LOOKAHEAD) + - rexstr(ƔAML_START) + rexstr(SOME_NEW_LINES) + - '(?:' + - rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) + rexstr(SOME_NEW_LINES) + - '){0,5}' + - rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) -); - -/* SEARCHES */ - -const FIND_ƔAML_LINE = unirex( - rexstr(NEW_LINE) + rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) -); - -/* STRING PROCESSING */ - -function processString (str) { - switch (str.charAt(0)) { - case '"': - return str.substring(1, str.length - 1); - case '\'': - return str - .substring(1, str.length - 1) - .replace(/''/g, '\''); - default: - return str; - } -} - -/* BIO PROCESSING */ - -export function processBio(content) { - content = content.replace(/"/g, '"').replace(/'/g, '\''); - let result = { - text: content, - metadata: [], - }; - let ɣaml = content.match(ƔAML_FRONTMATTER); - if (!ɣaml) { - return result; - } else { - ɣaml = ɣaml[0]; - } - const start = content.search(ƔAML_START); - const end = start + ɣaml.length - ɣaml.search(ƔAML_START); - result.text = content.substr(end); - let metadata = null; - let query = new RegExp(rexstr(FIND_ƔAML_LINE), 'g'); // Some browsers don't allow flags unless both args are strings - while ((metadata = query.exec(ɣaml))) { - result.metadata.push([ - processString(metadata[1]), - processString(metadata[2]), - ]); - } - return result; -} - -/* BIO CREATION */ - -export function createBio(note, data) { - if (!note) note = ''; - let frontmatter = ''; - if ((data && data.length) || note.match(/^\s*---\s+/)) { - if (!data) frontmatter = '---\n...\n'; - else { - frontmatter += '---\n'; - for (let i = 0; i < data.length; i++) { - let key = '' + data[i][0]; - let val = '' + data[i][1]; - - // Key processing - if (key === (key.match(ƔAML_SIMPLE_KEY) || [])[0]) /* do nothing */; - else if (key === (key.match(ANY_QUOTE_CHAR) || [])[0]) key = '"' + key + '"'; - else { - key = key - .replace(/'/g, '\'\'') - .replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '�'); - key = '\'' + key + '\''; - } - - // Value processing - if (val === (val.match(ƔAML_SIMPLE_VALUE) || [])[0]) /* do nothing */; - else if (val === (val.match(ANY_QUOTE_CHAR) || [])[0]) val = '"' + val + '"'; - else { - key = key - .replace(/'/g, '\'\'') - .replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '�'); - key = '\'' + key + '\''; - } - - frontmatter += key + ': ' + val + '\n'; - } - frontmatter += '...\n'; - } - } - return frontmatter + note; -} diff --git a/app/javascript/themes/glitch/util/counter.js b/app/javascript/themes/glitch/util/counter.js deleted file mode 100644 index 700ba2163..000000000 --- a/app/javascript/themes/glitch/util/counter.js +++ /dev/null @@ -1,9 +0,0 @@ -import { urlRegex } from './url_regex'; - -const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx'; - -export function countableText(inputText) { - return inputText - .replace(urlRegex, urlPlaceholder) - .replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3'); -}; diff --git a/app/javascript/themes/glitch/util/emoji/emoji_compressed.js b/app/javascript/themes/glitch/util/emoji/emoji_compressed.js deleted file mode 100644 index e5b834a74..000000000 --- a/app/javascript/themes/glitch/util/emoji/emoji_compressed.js +++ /dev/null @@ -1,93 +0,0 @@ -// @preval -// http://www.unicode.org/Public/emoji/5.0/emoji-test.txt -// This file contains the compressed version of the emoji data from -// both emoji_map.json and from emoji-mart's emojiIndex and data objects. -// It's designed to be emitted in an array format to take up less space -// over the wire. - -const { unicodeToFilename } = require('./unicode_to_filename'); -const { unicodeToUnifiedName } = require('./unicode_to_unified_name'); -const emojiMap = require('./emoji_map.json'); -const { emojiIndex } = require('emoji-mart'); -const { default: emojiMartData } = require('emoji-mart/dist/data'); - -const excluded = ['®', '©', '™']; -const skins = ['🏻', '🏼', '🏽', '🏾', '🏿']; -const shortcodeMap = {}; - -const shortCodesToEmojiData = {}; -const emojisWithoutShortCodes = []; - -Object.keys(emojiIndex.emojis).forEach(key => { - shortcodeMap[emojiIndex.emojis[key].native] = emojiIndex.emojis[key].id; -}); - -const stripModifiers = unicode => { - skins.forEach(tone => { - unicode = unicode.replace(tone, ''); - }); - - return unicode; -}; - -Object.keys(emojiMap).forEach(key => { - if (excluded.includes(key)) { - delete emojiMap[key]; - return; - } - - const normalizedKey = stripModifiers(key); - let shortcode = shortcodeMap[normalizedKey]; - - if (!shortcode) { - shortcode = shortcodeMap[normalizedKey + '\uFE0F']; - } - - const filename = emojiMap[key]; - - const filenameData = [key]; - - if (unicodeToFilename(key) !== filename) { - // filename can't be derived using unicodeToFilename - filenameData.push(filename); - } - - if (typeof shortcode === 'undefined') { - emojisWithoutShortCodes.push(filenameData); - } else { - if (!Array.isArray(shortCodesToEmojiData[shortcode])) { - shortCodesToEmojiData[shortcode] = [[]]; - } - shortCodesToEmojiData[shortcode][0].push(filenameData); - } -}); - -Object.keys(emojiIndex.emojis).forEach(key => { - const { native } = emojiIndex.emojis[key]; - let { short_names, search, unified } = emojiMartData.emojis[key]; - if (short_names[0] !== key) { - throw new Error('The compresser expects the first short_code to be the ' + - 'key. It may need to be rewritten if the emoji change such that this ' + - 'is no longer the case.'); - } - - short_names = short_names.slice(1); // first short name can be inferred from the key - - const searchData = [native, short_names, search]; - if (unicodeToUnifiedName(native) !== unified) { - // unified name can't be derived from unicodeToUnifiedName - searchData.push(unified); - } - - shortCodesToEmojiData[key].push(searchData); -}); - -// JSON.parse/stringify is to emulate what @preval is doing and avoid any -// inconsistent behavior in dev mode -module.exports = JSON.parse(JSON.stringify([ - shortCodesToEmojiData, - emojiMartData.skins, - emojiMartData.categories, - emojiMartData.short_names, - emojisWithoutShortCodes, -])); diff --git a/app/javascript/themes/glitch/util/emoji/emoji_map.json b/app/javascript/themes/glitch/util/emoji/emoji_map.json deleted file mode 100644 index 13753ba84..000000000 --- a/app/javascript/themes/glitch/util/emoji/emoji_map.json +++ /dev/null @@ -1 +0,0 @@ -{"😀":"1f600","😁":"1f601","😂":"1f602","🤣":"1f923","😃":"1f603","😄":"1f604","😅":"1f605","😆":"1f606","😉":"1f609","😊":"1f60a","😋":"1f60b","😎":"1f60e","😍":"1f60d","😘":"1f618","😗":"1f617","😙":"1f619","😚":"1f61a","☺":"263a","🙂":"1f642","🤗":"1f917","🤩":"1f929","🤔":"1f914","🤨":"1f928","😐":"1f610","😑":"1f611","😶":"1f636","🙄":"1f644","😏":"1f60f","😣":"1f623","😥":"1f625","😮":"1f62e","🤐":"1f910","😯":"1f62f","😪":"1f62a","😫":"1f62b","😴":"1f634","😌":"1f60c","😛":"1f61b","😜":"1f61c","😝":"1f61d","🤤":"1f924","😒":"1f612","😓":"1f613","😔":"1f614","😕":"1f615","🙃":"1f643","🤑":"1f911","😲":"1f632","☹":"2639","🙁":"1f641","😖":"1f616","😞":"1f61e","😟":"1f61f","😤":"1f624","😢":"1f622","😭":"1f62d","😦":"1f626","😧":"1f627","😨":"1f628","😩":"1f629","🤯":"1f92f","😬":"1f62c","😰":"1f630","😱":"1f631","😳":"1f633","🤪":"1f92a","😵":"1f635","😡":"1f621","😠":"1f620","🤬":"1f92c","😷":"1f637","🤒":"1f912","🤕":"1f915","🤢":"1f922","🤮":"1f92e","🤧":"1f927","😇":"1f607","🤠":"1f920","🤡":"1f921","🤥":"1f925","🤫":"1f92b","🤭":"1f92d","🧐":"1f9d0","🤓":"1f913","😈":"1f608","👿":"1f47f","👹":"1f479","👺":"1f47a","💀":"1f480","☠":"2620","👻":"1f47b","👽":"1f47d","👾":"1f47e","🤖":"1f916","💩":"1f4a9","😺":"1f63a","😸":"1f638","😹":"1f639","😻":"1f63b","😼":"1f63c","😽":"1f63d","🙀":"1f640","😿":"1f63f","😾":"1f63e","🙈":"1f648","🙉":"1f649","🙊":"1f64a","👶":"1f476","🧒":"1f9d2","👦":"1f466","👧":"1f467","🧑":"1f9d1","👨":"1f468","👩":"1f469","🧓":"1f9d3","👴":"1f474","👵":"1f475","👮":"1f46e","🕵":"1f575","💂":"1f482","👷":"1f477","🤴":"1f934","👸":"1f478","👳":"1f473","👲":"1f472","🧕":"1f9d5","🧔":"1f9d4","👱":"1f471","🤵":"1f935","👰":"1f470","🤰":"1f930","🤱":"1f931","👼":"1f47c","🎅":"1f385","🤶":"1f936","🧙":"1f9d9","🧚":"1f9da","🧛":"1f9db","🧜":"1f9dc","🧝":"1f9dd","🧞":"1f9de","🧟":"1f9df","🙍":"1f64d","🙎":"1f64e","🙅":"1f645","🙆":"1f646","💁":"1f481","🙋":"1f64b","🙇":"1f647","🤦":"1f926","🤷":"1f937","💆":"1f486","💇":"1f487","🚶":"1f6b6","🏃":"1f3c3","💃":"1f483","🕺":"1f57a","👯":"1f46f","🧖":"1f9d6","🧗":"1f9d7","🧘":"1f9d8","🛀":"1f6c0","🛌":"1f6cc","🕴":"1f574","🗣":"1f5e3","👤":"1f464","👥":"1f465","🤺":"1f93a","🏇":"1f3c7","⛷":"26f7","🏂":"1f3c2","🏌":"1f3cc","🏄":"1f3c4","🚣":"1f6a3","🏊":"1f3ca","⛹":"26f9","🏋":"1f3cb","🚴":"1f6b4","🚵":"1f6b5","🏎":"1f3ce","🏍":"1f3cd","🤸":"1f938","🤼":"1f93c","🤽":"1f93d","🤾":"1f93e","🤹":"1f939","👫":"1f46b","👬":"1f46c","👭":"1f46d","💏":"1f48f","💑":"1f491","👪":"1f46a","🤳":"1f933","💪":"1f4aa","👈":"1f448","👉":"1f449","☝":"261d","👆":"1f446","🖕":"1f595","👇":"1f447","✌":"270c","🤞":"1f91e","🖖":"1f596","🤘":"1f918","🤙":"1f919","🖐":"1f590","✋":"270b","👌":"1f44c","👍":"1f44d","👎":"1f44e","✊":"270a","👊":"1f44a","🤛":"1f91b","🤜":"1f91c","🤚":"1f91a","👋":"1f44b","🤟":"1f91f","✍":"270d","👏":"1f44f","👐":"1f450","🙌":"1f64c","🤲":"1f932","🙏":"1f64f","🤝":"1f91d","💅":"1f485","👂":"1f442","👃":"1f443","👣":"1f463","👀":"1f440","👁":"1f441","🧠":"1f9e0","👅":"1f445","👄":"1f444","💋":"1f48b","💘":"1f498","❤":"2764","💓":"1f493","💔":"1f494","💕":"1f495","💖":"1f496","💗":"1f497","💙":"1f499","💚":"1f49a","💛":"1f49b","🧡":"1f9e1","💜":"1f49c","🖤":"1f5a4","💝":"1f49d","💞":"1f49e","💟":"1f49f","❣":"2763","💌":"1f48c","💤":"1f4a4","💢":"1f4a2","💣":"1f4a3","💥":"1f4a5","💦":"1f4a6","💨":"1f4a8","💫":"1f4ab","💬":"1f4ac","🗨":"1f5e8","🗯":"1f5ef","💭":"1f4ad","🕳":"1f573","👓":"1f453","🕶":"1f576","👔":"1f454","👕":"1f455","👖":"1f456","🧣":"1f9e3","🧤":"1f9e4","🧥":"1f9e5","🧦":"1f9e6","👗":"1f457","👘":"1f458","👙":"1f459","👚":"1f45a","👛":"1f45b","👜":"1f45c","👝":"1f45d","🛍":"1f6cd","🎒":"1f392","👞":"1f45e","👟":"1f45f","👠":"1f460","👡":"1f461","👢":"1f462","👑":"1f451","👒":"1f452","🎩":"1f3a9","🎓":"1f393","🧢":"1f9e2","⛑":"26d1","📿":"1f4ff","💄":"1f484","💍":"1f48d","💎":"1f48e","🐵":"1f435","🐒":"1f412","🦍":"1f98d","🐶":"1f436","🐕":"1f415","🐩":"1f429","🐺":"1f43a","🦊":"1f98a","🐱":"1f431","🐈":"1f408","🦁":"1f981","🐯":"1f42f","🐅":"1f405","🐆":"1f406","🐴":"1f434","🐎":"1f40e","🦄":"1f984","🦓":"1f993","🦌":"1f98c","🐮":"1f42e","🐂":"1f402","🐃":"1f403","🐄":"1f404","🐷":"1f437","🐖":"1f416","🐗":"1f417","🐽":"1f43d","🐏":"1f40f","🐑":"1f411","🐐":"1f410","🐪":"1f42a","🐫":"1f42b","🦒":"1f992","🐘":"1f418","🦏":"1f98f","🐭":"1f42d","🐁":"1f401","🐀":"1f400","🐹":"1f439","🐰":"1f430","🐇":"1f407","🐿":"1f43f","🦔":"1f994","🦇":"1f987","🐻":"1f43b","🐨":"1f428","🐼":"1f43c","🐾":"1f43e","🦃":"1f983","🐔":"1f414","🐓":"1f413","🐣":"1f423","🐤":"1f424","🐥":"1f425","🐦":"1f426","🐧":"1f427","🕊":"1f54a","🦅":"1f985","🦆":"1f986","🦉":"1f989","🐸":"1f438","🐊":"1f40a","🐢":"1f422","🦎":"1f98e","🐍":"1f40d","🐲":"1f432","🐉":"1f409","🦕":"1f995","🦖":"1f996","🐳":"1f433","🐋":"1f40b","🐬":"1f42c","🐟":"1f41f","🐠":"1f420","🐡":"1f421","🦈":"1f988","🐙":"1f419","🐚":"1f41a","🦀":"1f980","🦐":"1f990","🦑":"1f991","🐌":"1f40c","🦋":"1f98b","🐛":"1f41b","🐜":"1f41c","🐝":"1f41d","🐞":"1f41e","🦗":"1f997","🕷":"1f577","🕸":"1f578","🦂":"1f982","💐":"1f490","🌸":"1f338","💮":"1f4ae","🏵":"1f3f5","🌹":"1f339","🥀":"1f940","🌺":"1f33a","🌻":"1f33b","🌼":"1f33c","🌷":"1f337","🌱":"1f331","🌲":"1f332","🌳":"1f333","🌴":"1f334","🌵":"1f335","🌾":"1f33e","🌿":"1f33f","☘":"2618","🍀":"1f340","🍁":"1f341","🍂":"1f342","🍃":"1f343","🍇":"1f347","🍈":"1f348","🍉":"1f349","🍊":"1f34a","🍋":"1f34b","🍌":"1f34c","🍍":"1f34d","🍎":"1f34e","🍏":"1f34f","🍐":"1f350","🍑":"1f351","🍒":"1f352","🍓":"1f353","🥝":"1f95d","🍅":"1f345","🥥":"1f965","🥑":"1f951","🍆":"1f346","🥔":"1f954","🥕":"1f955","🌽":"1f33d","🌶":"1f336","🥒":"1f952","🥦":"1f966","🍄":"1f344","🥜":"1f95c","🌰":"1f330","🍞":"1f35e","🥐":"1f950","🥖":"1f956","🥨":"1f968","🥞":"1f95e","🧀":"1f9c0","🍖":"1f356","🍗":"1f357","🥩":"1f969","🥓":"1f953","🍔":"1f354","🍟":"1f35f","🍕":"1f355","🌭":"1f32d","🥪":"1f96a","🌮":"1f32e","🌯":"1f32f","🥙":"1f959","🥚":"1f95a","🍳":"1f373","🥘":"1f958","🍲":"1f372","🥣":"1f963","🥗":"1f957","🍿":"1f37f","🥫":"1f96b","🍱":"1f371","🍘":"1f358","🍙":"1f359","🍚":"1f35a","🍛":"1f35b","🍜":"1f35c","🍝":"1f35d","🍠":"1f360","🍢":"1f362","🍣":"1f363","🍤":"1f364","🍥":"1f365","🍡":"1f361","🥟":"1f95f","🥠":"1f960","🥡":"1f961","🍦":"1f366","🍧":"1f367","🍨":"1f368","🍩":"1f369","🍪":"1f36a","🎂":"1f382","🍰":"1f370","🥧":"1f967","🍫":"1f36b","🍬":"1f36c","🍭":"1f36d","🍮":"1f36e","🍯":"1f36f","🍼":"1f37c","🥛":"1f95b","☕":"2615","🍵":"1f375","🍶":"1f376","🍾":"1f37e","🍷":"1f377","🍸":"1f378","🍹":"1f379","🍺":"1f37a","🍻":"1f37b","🥂":"1f942","🥃":"1f943","🥤":"1f964","🥢":"1f962","🍽":"1f37d","🍴":"1f374","🥄":"1f944","🔪":"1f52a","🏺":"1f3fa","🌍":"1f30d","🌎":"1f30e","🌏":"1f30f","🌐":"1f310","🗺":"1f5fa","🗾":"1f5fe","🏔":"1f3d4","⛰":"26f0","🌋":"1f30b","🗻":"1f5fb","🏕":"1f3d5","🏖":"1f3d6","🏜":"1f3dc","🏝":"1f3dd","🏞":"1f3de","🏟":"1f3df","🏛":"1f3db","🏗":"1f3d7","🏘":"1f3d8","🏙":"1f3d9","🏚":"1f3da","🏠":"1f3e0","🏡":"1f3e1","🏢":"1f3e2","🏣":"1f3e3","🏤":"1f3e4","🏥":"1f3e5","🏦":"1f3e6","🏨":"1f3e8","🏩":"1f3e9","🏪":"1f3ea","🏫":"1f3eb","🏬":"1f3ec","🏭":"1f3ed","🏯":"1f3ef","🏰":"1f3f0","💒":"1f492","🗼":"1f5fc","🗽":"1f5fd","⛪":"26ea","🕌":"1f54c","🕍":"1f54d","⛩":"26e9","🕋":"1f54b","⛲":"26f2","⛺":"26fa","🌁":"1f301","🌃":"1f303","🌄":"1f304","🌅":"1f305","🌆":"1f306","🌇":"1f307","🌉":"1f309","♨":"2668","🌌":"1f30c","🎠":"1f3a0","🎡":"1f3a1","🎢":"1f3a2","💈":"1f488","🎪":"1f3aa","🎭":"1f3ad","🖼":"1f5bc","🎨":"1f3a8","🎰":"1f3b0","🚂":"1f682","🚃":"1f683","🚄":"1f684","🚅":"1f685","🚆":"1f686","🚇":"1f687","🚈":"1f688","🚉":"1f689","🚊":"1f68a","🚝":"1f69d","🚞":"1f69e","🚋":"1f68b","🚌":"1f68c","🚍":"1f68d","🚎":"1f68e","🚐":"1f690","🚑":"1f691","🚒":"1f692","🚓":"1f693","🚔":"1f694","🚕":"1f695","🚖":"1f696","🚗":"1f697","🚘":"1f698","🚙":"1f699","🚚":"1f69a","🚛":"1f69b","🚜":"1f69c","🚲":"1f6b2","🛴":"1f6f4","🛵":"1f6f5","🚏":"1f68f","🛣":"1f6e3","🛤":"1f6e4","⛽":"26fd","🚨":"1f6a8","🚥":"1f6a5","🚦":"1f6a6","🚧":"1f6a7","🛑":"1f6d1","⚓":"2693","⛵":"26f5","🛶":"1f6f6","🚤":"1f6a4","🛳":"1f6f3","⛴":"26f4","🛥":"1f6e5","🚢":"1f6a2","✈":"2708","🛩":"1f6e9","🛫":"1f6eb","🛬":"1f6ec","💺":"1f4ba","🚁":"1f681","🚟":"1f69f","🚠":"1f6a0","🚡":"1f6a1","🛰":"1f6f0","🚀":"1f680","🛸":"1f6f8","🛎":"1f6ce","🚪":"1f6aa","🛏":"1f6cf","🛋":"1f6cb","🚽":"1f6bd","🚿":"1f6bf","🛁":"1f6c1","⌛":"231b","⏳":"23f3","⌚":"231a","⏰":"23f0","⏱":"23f1","⏲":"23f2","🕰":"1f570","🕛":"1f55b","🕧":"1f567","🕐":"1f550","🕜":"1f55c","🕑":"1f551","🕝":"1f55d","🕒":"1f552","🕞":"1f55e","🕓":"1f553","🕟":"1f55f","🕔":"1f554","🕠":"1f560","🕕":"1f555","🕡":"1f561","🕖":"1f556","🕢":"1f562","🕗":"1f557","🕣":"1f563","🕘":"1f558","🕤":"1f564","🕙":"1f559","🕥":"1f565","🕚":"1f55a","🕦":"1f566","🌑":"1f311","🌒":"1f312","🌓":"1f313","🌔":"1f314","🌕":"1f315","🌖":"1f316","🌗":"1f317","🌘":"1f318","🌙":"1f319","🌚":"1f31a","🌛":"1f31b","🌜":"1f31c","🌡":"1f321","☀":"2600","🌝":"1f31d","🌞":"1f31e","⭐":"2b50","🌟":"1f31f","🌠":"1f320","☁":"2601","⛅":"26c5","⛈":"26c8","🌤":"1f324","🌥":"1f325","🌦":"1f326","🌧":"1f327","🌨":"1f328","🌩":"1f329","🌪":"1f32a","🌫":"1f32b","🌬":"1f32c","🌀":"1f300","🌈":"1f308","🌂":"1f302","☂":"2602","☔":"2614","⛱":"26f1","⚡":"26a1","❄":"2744","☃":"2603","⛄":"26c4","☄":"2604","🔥":"1f525","💧":"1f4a7","🌊":"1f30a","🎃":"1f383","🎄":"1f384","🎆":"1f386","🎇":"1f387","✨":"2728","🎈":"1f388","🎉":"1f389","🎊":"1f38a","🎋":"1f38b","🎍":"1f38d","🎎":"1f38e","🎏":"1f38f","🎐":"1f390","🎑":"1f391","🎀":"1f380","🎁":"1f381","🎗":"1f397","🎟":"1f39f","🎫":"1f3ab","🎖":"1f396","🏆":"1f3c6","🏅":"1f3c5","🥇":"1f947","🥈":"1f948","🥉":"1f949","⚽":"26bd","⚾":"26be","🏀":"1f3c0","🏐":"1f3d0","🏈":"1f3c8","🏉":"1f3c9","🎾":"1f3be","🎱":"1f3b1","🎳":"1f3b3","🏏":"1f3cf","🏑":"1f3d1","🏒":"1f3d2","🏓":"1f3d3","🏸":"1f3f8","🥊":"1f94a","🥋":"1f94b","🥅":"1f945","🎯":"1f3af","⛳":"26f3","⛸":"26f8","🎣":"1f3a3","🎽":"1f3bd","🎿":"1f3bf","🛷":"1f6f7","🥌":"1f94c","🎮":"1f3ae","🕹":"1f579","🎲":"1f3b2","♠":"2660","♥":"2665","♦":"2666","♣":"2663","🃏":"1f0cf","🀄":"1f004","🎴":"1f3b4","🔇":"1f507","🔈":"1f508","🔉":"1f509","🔊":"1f50a","📢":"1f4e2","📣":"1f4e3","📯":"1f4ef","🔔":"1f514","🔕":"1f515","🎼":"1f3bc","🎵":"1f3b5","🎶":"1f3b6","🎙":"1f399","🎚":"1f39a","🎛":"1f39b","🎤":"1f3a4","🎧":"1f3a7","📻":"1f4fb","🎷":"1f3b7","🎸":"1f3b8","🎹":"1f3b9","🎺":"1f3ba","🎻":"1f3bb","🥁":"1f941","📱":"1f4f1","📲":"1f4f2","☎":"260e","📞":"1f4de","📟":"1f4df","📠":"1f4e0","🔋":"1f50b","🔌":"1f50c","💻":"1f4bb","🖥":"1f5a5","🖨":"1f5a8","⌨":"2328","🖱":"1f5b1","🖲":"1f5b2","💽":"1f4bd","💾":"1f4be","💿":"1f4bf","📀":"1f4c0","🎥":"1f3a5","🎞":"1f39e","📽":"1f4fd","🎬":"1f3ac","📺":"1f4fa","📷":"1f4f7","📸":"1f4f8","📹":"1f4f9","📼":"1f4fc","🔍":"1f50d","🔎":"1f50e","🔬":"1f52c","🔭":"1f52d","📡":"1f4e1","🕯":"1f56f","💡":"1f4a1","🔦":"1f526","🏮":"1f3ee","📔":"1f4d4","📕":"1f4d5","📖":"1f4d6","📗":"1f4d7","📘":"1f4d8","📙":"1f4d9","📚":"1f4da","📓":"1f4d3","📒":"1f4d2","📃":"1f4c3","📜":"1f4dc","📄":"1f4c4","📰":"1f4f0","🗞":"1f5de","📑":"1f4d1","🔖":"1f516","🏷":"1f3f7","💰":"1f4b0","💴":"1f4b4","💵":"1f4b5","💶":"1f4b6","💷":"1f4b7","💸":"1f4b8","💳":"1f4b3","💹":"1f4b9","💱":"1f4b1","💲":"1f4b2","✉":"2709","📧":"1f4e7","📨":"1f4e8","📩":"1f4e9","📤":"1f4e4","📥":"1f4e5","📦":"1f4e6","📫":"1f4eb","📪":"1f4ea","📬":"1f4ec","📭":"1f4ed","📮":"1f4ee","🗳":"1f5f3","✏":"270f","✒":"2712","🖋":"1f58b","🖊":"1f58a","🖌":"1f58c","🖍":"1f58d","📝":"1f4dd","💼":"1f4bc","📁":"1f4c1","📂":"1f4c2","🗂":"1f5c2","📅":"1f4c5","📆":"1f4c6","🗒":"1f5d2","🗓":"1f5d3","📇":"1f4c7","📈":"1f4c8","📉":"1f4c9","📊":"1f4ca","📋":"1f4cb","📌":"1f4cc","📍":"1f4cd","📎":"1f4ce","🖇":"1f587","📏":"1f4cf","📐":"1f4d0","✂":"2702","🗃":"1f5c3","🗄":"1f5c4","🗑":"1f5d1","🔒":"1f512","🔓":"1f513","🔏":"1f50f","🔐":"1f510","🔑":"1f511","🗝":"1f5dd","🔨":"1f528","⛏":"26cf","⚒":"2692","🛠":"1f6e0","🗡":"1f5e1","⚔":"2694","🔫":"1f52b","🏹":"1f3f9","🛡":"1f6e1","🔧":"1f527","🔩":"1f529","⚙":"2699","🗜":"1f5dc","⚗":"2697","⚖":"2696","🔗":"1f517","⛓":"26d3","💉":"1f489","💊":"1f48a","🚬":"1f6ac","⚰":"26b0","⚱":"26b1","🗿":"1f5ff","🛢":"1f6e2","🔮":"1f52e","🛒":"1f6d2","🏧":"1f3e7","🚮":"1f6ae","🚰":"1f6b0","♿":"267f","🚹":"1f6b9","🚺":"1f6ba","🚻":"1f6bb","🚼":"1f6bc","🚾":"1f6be","🛂":"1f6c2","🛃":"1f6c3","🛄":"1f6c4","🛅":"1f6c5","⚠":"26a0","🚸":"1f6b8","⛔":"26d4","🚫":"1f6ab","🚳":"1f6b3","🚭":"1f6ad","🚯":"1f6af","🚱":"1f6b1","🚷":"1f6b7","📵":"1f4f5","🔞":"1f51e","☢":"2622","☣":"2623","⬆":"2b06","↗":"2197","➡":"27a1","↘":"2198","⬇":"2b07","↙":"2199","⬅":"2b05","↖":"2196","↕":"2195","↔":"2194","↩":"21a9","↪":"21aa","⤴":"2934","⤵":"2935","🔃":"1f503","🔄":"1f504","🔙":"1f519","🔚":"1f51a","🔛":"1f51b","🔜":"1f51c","🔝":"1f51d","🛐":"1f6d0","⚛":"269b","🕉":"1f549","✡":"2721","☸":"2638","☯":"262f","✝":"271d","☦":"2626","☪":"262a","☮":"262e","🕎":"1f54e","🔯":"1f52f","♈":"2648","♉":"2649","♊":"264a","♋":"264b","♌":"264c","♍":"264d","♎":"264e","♏":"264f","♐":"2650","♑":"2651","♒":"2652","♓":"2653","⛎":"26ce","🔀":"1f500","🔁":"1f501","🔂":"1f502","▶":"25b6","⏩":"23e9","⏭":"23ed","⏯":"23ef","◀":"25c0","⏪":"23ea","⏮":"23ee","🔼":"1f53c","⏫":"23eb","🔽":"1f53d","⏬":"23ec","⏸":"23f8","⏹":"23f9","⏺":"23fa","⏏":"23cf","🎦":"1f3a6","🔅":"1f505","🔆":"1f506","📶":"1f4f6","📳":"1f4f3","📴":"1f4f4","♀":"2640","♂":"2642","⚕":"2695","♻":"267b","⚜":"269c","🔱":"1f531","📛":"1f4db","🔰":"1f530","⭕":"2b55","✅":"2705","☑":"2611","✔":"2714","✖":"2716","❌":"274c","❎":"274e","➕":"2795","➖":"2796","➗":"2797","➰":"27b0","➿":"27bf","〽":"303d","✳":"2733","✴":"2734","❇":"2747","‼":"203c","⁉":"2049","❓":"2753","❔":"2754","❕":"2755","❗":"2757","〰":"3030","©":"a9","®":"ae","™":"2122","🔟":"1f51f","💯":"1f4af","🔠":"1f520","🔡":"1f521","🔢":"1f522","🔣":"1f523","🔤":"1f524","🅰":"1f170","🆎":"1f18e","🅱":"1f171","🆑":"1f191","🆒":"1f192","🆓":"1f193","ℹ":"2139","🆔":"1f194","Ⓜ":"24c2","🆕":"1f195","🆖":"1f196","🅾":"1f17e","🆗":"1f197","🅿":"1f17f","🆘":"1f198","🆙":"1f199","🆚":"1f19a","🈁":"1f201","🈂":"1f202","🈷":"1f237","🈶":"1f236","🈯":"1f22f","🉐":"1f250","🈹":"1f239","🈚":"1f21a","🈲":"1f232","🉑":"1f251","🈸":"1f238","🈴":"1f234","🈳":"1f233","㊗":"3297","㊙":"3299","🈺":"1f23a","🈵":"1f235","▪":"25aa","▫":"25ab","◻":"25fb","◼":"25fc","◽":"25fd","◾":"25fe","⬛":"2b1b","⬜":"2b1c","🔶":"1f536","🔷":"1f537","🔸":"1f538","🔹":"1f539","🔺":"1f53a","🔻":"1f53b","💠":"1f4a0","🔘":"1f518","🔲":"1f532","🔳":"1f533","⚪":"26aa","⚫":"26ab","🔴":"1f534","🔵":"1f535","🏁":"1f3c1","🚩":"1f6a9","🎌":"1f38c","🏴":"1f3f4","🏳":"1f3f3","☺️":"263a","☹️":"2639","☠️":"2620","👶🏻":"1f476-1f3fb","👶🏼":"1f476-1f3fc","👶🏽":"1f476-1f3fd","👶🏾":"1f476-1f3fe","👶🏿":"1f476-1f3ff","🧒🏻":"1f9d2-1f3fb","🧒🏼":"1f9d2-1f3fc","🧒🏽":"1f9d2-1f3fd","🧒🏾":"1f9d2-1f3fe","🧒🏿":"1f9d2-1f3ff","👦🏻":"1f466-1f3fb","👦🏼":"1f466-1f3fc","👦🏽":"1f466-1f3fd","👦🏾":"1f466-1f3fe","👦🏿":"1f466-1f3ff","👧🏻":"1f467-1f3fb","👧🏼":"1f467-1f3fc","👧🏽":"1f467-1f3fd","👧🏾":"1f467-1f3fe","👧🏿":"1f467-1f3ff","🧑🏻":"1f9d1-1f3fb","🧑🏼":"1f9d1-1f3fc","🧑🏽":"1f9d1-1f3fd","🧑🏾":"1f9d1-1f3fe","🧑🏿":"1f9d1-1f3ff","👨🏻":"1f468-1f3fb","👨🏼":"1f468-1f3fc","👨🏽":"1f468-1f3fd","👨🏾":"1f468-1f3fe","👨🏿":"1f468-1f3ff","👩🏻":"1f469-1f3fb","👩🏼":"1f469-1f3fc","👩🏽":"1f469-1f3fd","👩🏾":"1f469-1f3fe","👩🏿":"1f469-1f3ff","🧓🏻":"1f9d3-1f3fb","🧓🏼":"1f9d3-1f3fc","🧓🏽":"1f9d3-1f3fd","🧓🏾":"1f9d3-1f3fe","🧓🏿":"1f9d3-1f3ff","👴🏻":"1f474-1f3fb","👴🏼":"1f474-1f3fc","👴🏽":"1f474-1f3fd","👴🏾":"1f474-1f3fe","👴🏿":"1f474-1f3ff","👵🏻":"1f475-1f3fb","👵🏼":"1f475-1f3fc","👵🏽":"1f475-1f3fd","👵🏾":"1f475-1f3fe","👵🏿":"1f475-1f3ff","👮🏻":"1f46e-1f3fb","👮🏼":"1f46e-1f3fc","👮🏽":"1f46e-1f3fd","👮🏾":"1f46e-1f3fe","👮🏿":"1f46e-1f3ff","🕵️":"1f575","🕵🏻":"1f575-1f3fb","🕵🏼":"1f575-1f3fc","🕵🏽":"1f575-1f3fd","🕵🏾":"1f575-1f3fe","🕵🏿":"1f575-1f3ff","💂🏻":"1f482-1f3fb","💂🏼":"1f482-1f3fc","💂🏽":"1f482-1f3fd","💂🏾":"1f482-1f3fe","💂🏿":"1f482-1f3ff","👷🏻":"1f477-1f3fb","👷🏼":"1f477-1f3fc","👷🏽":"1f477-1f3fd","👷🏾":"1f477-1f3fe","👷🏿":"1f477-1f3ff","🤴🏻":"1f934-1f3fb","🤴🏼":"1f934-1f3fc","🤴🏽":"1f934-1f3fd","🤴🏾":"1f934-1f3fe","🤴🏿":"1f934-1f3ff","👸🏻":"1f478-1f3fb","👸🏼":"1f478-1f3fc","👸🏽":"1f478-1f3fd","👸🏾":"1f478-1f3fe","👸🏿":"1f478-1f3ff","👳🏻":"1f473-1f3fb","👳🏼":"1f473-1f3fc","👳🏽":"1f473-1f3fd","👳🏾":"1f473-1f3fe","👳🏿":"1f473-1f3ff","👲🏻":"1f472-1f3fb","👲🏼":"1f472-1f3fc","👲🏽":"1f472-1f3fd","👲🏾":"1f472-1f3fe","👲🏿":"1f472-1f3ff","🧕🏻":"1f9d5-1f3fb","🧕🏼":"1f9d5-1f3fc","🧕🏽":"1f9d5-1f3fd","🧕🏾":"1f9d5-1f3fe","🧕🏿":"1f9d5-1f3ff","🧔🏻":"1f9d4-1f3fb","🧔🏼":"1f9d4-1f3fc","🧔🏽":"1f9d4-1f3fd","🧔🏾":"1f9d4-1f3fe","🧔🏿":"1f9d4-1f3ff","👱🏻":"1f471-1f3fb","👱🏼":"1f471-1f3fc","👱🏽":"1f471-1f3fd","👱🏾":"1f471-1f3fe","👱🏿":"1f471-1f3ff","🤵🏻":"1f935-1f3fb","🤵🏼":"1f935-1f3fc","🤵🏽":"1f935-1f3fd","🤵🏾":"1f935-1f3fe","🤵🏿":"1f935-1f3ff","👰🏻":"1f470-1f3fb","👰🏼":"1f470-1f3fc","👰🏽":"1f470-1f3fd","👰🏾":"1f470-1f3fe","👰🏿":"1f470-1f3ff","🤰🏻":"1f930-1f3fb","🤰🏼":"1f930-1f3fc","🤰🏽":"1f930-1f3fd","🤰🏾":"1f930-1f3fe","🤰🏿":"1f930-1f3ff","🤱🏻":"1f931-1f3fb","🤱🏼":"1f931-1f3fc","🤱🏽":"1f931-1f3fd","🤱🏾":"1f931-1f3fe","🤱🏿":"1f931-1f3ff","👼🏻":"1f47c-1f3fb","👼🏼":"1f47c-1f3fc","👼🏽":"1f47c-1f3fd","👼🏾":"1f47c-1f3fe","👼🏿":"1f47c-1f3ff","🎅🏻":"1f385-1f3fb","🎅🏼":"1f385-1f3fc","🎅🏽":"1f385-1f3fd","🎅🏾":"1f385-1f3fe","🎅🏿":"1f385-1f3ff","🤶🏻":"1f936-1f3fb","🤶🏼":"1f936-1f3fc","🤶🏽":"1f936-1f3fd","🤶🏾":"1f936-1f3fe","🤶🏿":"1f936-1f3ff","🧙🏻":"1f9d9-1f3fb","🧙🏼":"1f9d9-1f3fc","🧙🏽":"1f9d9-1f3fd","🧙🏾":"1f9d9-1f3fe","🧙🏿":"1f9d9-1f3ff","🧚🏻":"1f9da-1f3fb","🧚🏼":"1f9da-1f3fc","🧚🏽":"1f9da-1f3fd","🧚🏾":"1f9da-1f3fe","🧚🏿":"1f9da-1f3ff","🧛🏻":"1f9db-1f3fb","🧛🏼":"1f9db-1f3fc","🧛🏽":"1f9db-1f3fd","🧛🏾":"1f9db-1f3fe","🧛🏿":"1f9db-1f3ff","🧜🏻":"1f9dc-1f3fb","🧜🏼":"1f9dc-1f3fc","🧜🏽":"1f9dc-1f3fd","🧜🏾":"1f9dc-1f3fe","🧜🏿":"1f9dc-1f3ff","🧝🏻":"1f9dd-1f3fb","🧝🏼":"1f9dd-1f3fc","🧝🏽":"1f9dd-1f3fd","🧝🏾":"1f9dd-1f3fe","🧝🏿":"1f9dd-1f3ff","🙍🏻":"1f64d-1f3fb","🙍🏼":"1f64d-1f3fc","🙍🏽":"1f64d-1f3fd","🙍🏾":"1f64d-1f3fe","🙍🏿":"1f64d-1f3ff","🙎🏻":"1f64e-1f3fb","🙎🏼":"1f64e-1f3fc","🙎🏽":"1f64e-1f3fd","🙎🏾":"1f64e-1f3fe","🙎🏿":"1f64e-1f3ff","🙅🏻":"1f645-1f3fb","🙅🏼":"1f645-1f3fc","🙅🏽":"1f645-1f3fd","🙅🏾":"1f645-1f3fe","🙅🏿":"1f645-1f3ff","🙆🏻":"1f646-1f3fb","🙆🏼":"1f646-1f3fc","🙆🏽":"1f646-1f3fd","🙆🏾":"1f646-1f3fe","🙆🏿":"1f646-1f3ff","💁🏻":"1f481-1f3fb","💁🏼":"1f481-1f3fc","💁🏽":"1f481-1f3fd","💁🏾":"1f481-1f3fe","💁🏿":"1f481-1f3ff","🙋🏻":"1f64b-1f3fb","🙋🏼":"1f64b-1f3fc","🙋🏽":"1f64b-1f3fd","🙋🏾":"1f64b-1f3fe","🙋🏿":"1f64b-1f3ff","🙇🏻":"1f647-1f3fb","🙇🏼":"1f647-1f3fc","🙇🏽":"1f647-1f3fd","🙇🏾":"1f647-1f3fe","🙇🏿":"1f647-1f3ff","🤦🏻":"1f926-1f3fb","🤦🏼":"1f926-1f3fc","🤦🏽":"1f926-1f3fd","🤦🏾":"1f926-1f3fe","🤦🏿":"1f926-1f3ff","🤷🏻":"1f937-1f3fb","🤷🏼":"1f937-1f3fc","🤷🏽":"1f937-1f3fd","🤷🏾":"1f937-1f3fe","🤷🏿":"1f937-1f3ff","💆🏻":"1f486-1f3fb","💆🏼":"1f486-1f3fc","💆🏽":"1f486-1f3fd","💆🏾":"1f486-1f3fe","💆🏿":"1f486-1f3ff","💇🏻":"1f487-1f3fb","💇🏼":"1f487-1f3fc","💇🏽":"1f487-1f3fd","💇🏾":"1f487-1f3fe","💇🏿":"1f487-1f3ff","🚶🏻":"1f6b6-1f3fb","🚶🏼":"1f6b6-1f3fc","🚶🏽":"1f6b6-1f3fd","🚶🏾":"1f6b6-1f3fe","🚶🏿":"1f6b6-1f3ff","🏃🏻":"1f3c3-1f3fb","🏃🏼":"1f3c3-1f3fc","🏃🏽":"1f3c3-1f3fd","🏃🏾":"1f3c3-1f3fe","🏃🏿":"1f3c3-1f3ff","💃🏻":"1f483-1f3fb","💃🏼":"1f483-1f3fc","💃🏽":"1f483-1f3fd","💃🏾":"1f483-1f3fe","💃🏿":"1f483-1f3ff","🕺🏻":"1f57a-1f3fb","🕺🏼":"1f57a-1f3fc","🕺🏽":"1f57a-1f3fd","🕺🏾":"1f57a-1f3fe","🕺🏿":"1f57a-1f3ff","🧖🏻":"1f9d6-1f3fb","🧖🏼":"1f9d6-1f3fc","🧖🏽":"1f9d6-1f3fd","🧖🏾":"1f9d6-1f3fe","🧖🏿":"1f9d6-1f3ff","🧗🏻":"1f9d7-1f3fb","🧗🏼":"1f9d7-1f3fc","🧗🏽":"1f9d7-1f3fd","🧗🏾":"1f9d7-1f3fe","🧗🏿":"1f9d7-1f3ff","🧘🏻":"1f9d8-1f3fb","🧘🏼":"1f9d8-1f3fc","🧘🏽":"1f9d8-1f3fd","🧘🏾":"1f9d8-1f3fe","🧘🏿":"1f9d8-1f3ff","🛀🏻":"1f6c0-1f3fb","🛀🏼":"1f6c0-1f3fc","🛀🏽":"1f6c0-1f3fd","🛀🏾":"1f6c0-1f3fe","🛀🏿":"1f6c0-1f3ff","🛌🏻":"1f6cc-1f3fb","🛌🏼":"1f6cc-1f3fc","🛌🏽":"1f6cc-1f3fd","🛌🏾":"1f6cc-1f3fe","🛌🏿":"1f6cc-1f3ff","🕴️":"1f574","🕴🏻":"1f574-1f3fb","🕴🏼":"1f574-1f3fc","🕴🏽":"1f574-1f3fd","🕴🏾":"1f574-1f3fe","🕴🏿":"1f574-1f3ff","🗣️":"1f5e3","🏇🏻":"1f3c7-1f3fb","🏇🏼":"1f3c7-1f3fc","🏇🏽":"1f3c7-1f3fd","🏇🏾":"1f3c7-1f3fe","🏇🏿":"1f3c7-1f3ff","⛷️":"26f7","🏂🏻":"1f3c2-1f3fb","🏂🏼":"1f3c2-1f3fc","🏂🏽":"1f3c2-1f3fd","🏂🏾":"1f3c2-1f3fe","🏂🏿":"1f3c2-1f3ff","🏌️":"1f3cc","🏌🏻":"1f3cc-1f3fb","🏌🏼":"1f3cc-1f3fc","🏌🏽":"1f3cc-1f3fd","🏌🏾":"1f3cc-1f3fe","🏌🏿":"1f3cc-1f3ff","🏄🏻":"1f3c4-1f3fb","🏄🏼":"1f3c4-1f3fc","🏄🏽":"1f3c4-1f3fd","🏄🏾":"1f3c4-1f3fe","🏄🏿":"1f3c4-1f3ff","🚣🏻":"1f6a3-1f3fb","🚣🏼":"1f6a3-1f3fc","🚣🏽":"1f6a3-1f3fd","🚣🏾":"1f6a3-1f3fe","🚣🏿":"1f6a3-1f3ff","🏊🏻":"1f3ca-1f3fb","🏊🏼":"1f3ca-1f3fc","🏊🏽":"1f3ca-1f3fd","🏊🏾":"1f3ca-1f3fe","🏊🏿":"1f3ca-1f3ff","⛹️":"26f9","⛹🏻":"26f9-1f3fb","⛹🏼":"26f9-1f3fc","⛹🏽":"26f9-1f3fd","⛹🏾":"26f9-1f3fe","⛹🏿":"26f9-1f3ff","🏋️":"1f3cb","🏋🏻":"1f3cb-1f3fb","🏋🏼":"1f3cb-1f3fc","🏋🏽":"1f3cb-1f3fd","🏋🏾":"1f3cb-1f3fe","🏋🏿":"1f3cb-1f3ff","🚴🏻":"1f6b4-1f3fb","🚴🏼":"1f6b4-1f3fc","🚴🏽":"1f6b4-1f3fd","🚴🏾":"1f6b4-1f3fe","🚴🏿":"1f6b4-1f3ff","🚵🏻":"1f6b5-1f3fb","🚵🏼":"1f6b5-1f3fc","🚵🏽":"1f6b5-1f3fd","🚵🏾":"1f6b5-1f3fe","🚵🏿":"1f6b5-1f3ff","🏎️":"1f3ce","🏍️":"1f3cd","🤸🏻":"1f938-1f3fb","🤸🏼":"1f938-1f3fc","🤸🏽":"1f938-1f3fd","🤸🏾":"1f938-1f3fe","🤸🏿":"1f938-1f3ff","🤽🏻":"1f93d-1f3fb","🤽🏼":"1f93d-1f3fc","🤽🏽":"1f93d-1f3fd","🤽🏾":"1f93d-1f3fe","🤽🏿":"1f93d-1f3ff","🤾🏻":"1f93e-1f3fb","🤾🏼":"1f93e-1f3fc","🤾🏽":"1f93e-1f3fd","🤾🏾":"1f93e-1f3fe","🤾🏿":"1f93e-1f3ff","🤹🏻":"1f939-1f3fb","🤹🏼":"1f939-1f3fc","🤹🏽":"1f939-1f3fd","🤹🏾":"1f939-1f3fe","🤹🏿":"1f939-1f3ff","🤳🏻":"1f933-1f3fb","🤳🏼":"1f933-1f3fc","🤳🏽":"1f933-1f3fd","🤳🏾":"1f933-1f3fe","🤳🏿":"1f933-1f3ff","💪🏻":"1f4aa-1f3fb","💪🏼":"1f4aa-1f3fc","💪🏽":"1f4aa-1f3fd","💪🏾":"1f4aa-1f3fe","💪🏿":"1f4aa-1f3ff","👈🏻":"1f448-1f3fb","👈🏼":"1f448-1f3fc","👈🏽":"1f448-1f3fd","👈🏾":"1f448-1f3fe","👈🏿":"1f448-1f3ff","👉🏻":"1f449-1f3fb","👉🏼":"1f449-1f3fc","👉🏽":"1f449-1f3fd","👉🏾":"1f449-1f3fe","👉🏿":"1f449-1f3ff","☝️":"261d","☝🏻":"261d-1f3fb","☝🏼":"261d-1f3fc","☝🏽":"261d-1f3fd","☝🏾":"261d-1f3fe","☝🏿":"261d-1f3ff","👆🏻":"1f446-1f3fb","👆🏼":"1f446-1f3fc","👆🏽":"1f446-1f3fd","👆🏾":"1f446-1f3fe","👆🏿":"1f446-1f3ff","🖕🏻":"1f595-1f3fb","🖕🏼":"1f595-1f3fc","🖕🏽":"1f595-1f3fd","🖕🏾":"1f595-1f3fe","🖕🏿":"1f595-1f3ff","👇🏻":"1f447-1f3fb","👇🏼":"1f447-1f3fc","👇🏽":"1f447-1f3fd","👇🏾":"1f447-1f3fe","👇🏿":"1f447-1f3ff","✌️":"270c","✌🏻":"270c-1f3fb","✌🏼":"270c-1f3fc","✌🏽":"270c-1f3fd","✌🏾":"270c-1f3fe","✌🏿":"270c-1f3ff","🤞🏻":"1f91e-1f3fb","🤞🏼":"1f91e-1f3fc","🤞🏽":"1f91e-1f3fd","🤞🏾":"1f91e-1f3fe","🤞🏿":"1f91e-1f3ff","🖖🏻":"1f596-1f3fb","🖖🏼":"1f596-1f3fc","🖖🏽":"1f596-1f3fd","🖖🏾":"1f596-1f3fe","🖖🏿":"1f596-1f3ff","🤘🏻":"1f918-1f3fb","🤘🏼":"1f918-1f3fc","🤘🏽":"1f918-1f3fd","🤘🏾":"1f918-1f3fe","🤘🏿":"1f918-1f3ff","🤙🏻":"1f919-1f3fb","🤙🏼":"1f919-1f3fc","🤙🏽":"1f919-1f3fd","🤙🏾":"1f919-1f3fe","🤙🏿":"1f919-1f3ff","🖐️":"1f590","🖐🏻":"1f590-1f3fb","🖐🏼":"1f590-1f3fc","🖐🏽":"1f590-1f3fd","🖐🏾":"1f590-1f3fe","🖐🏿":"1f590-1f3ff","✋🏻":"270b-1f3fb","✋🏼":"270b-1f3fc","✋🏽":"270b-1f3fd","✋🏾":"270b-1f3fe","✋🏿":"270b-1f3ff","👌🏻":"1f44c-1f3fb","👌🏼":"1f44c-1f3fc","👌🏽":"1f44c-1f3fd","👌🏾":"1f44c-1f3fe","👌🏿":"1f44c-1f3ff","👍🏻":"1f44d-1f3fb","👍🏼":"1f44d-1f3fc","👍🏽":"1f44d-1f3fd","👍🏾":"1f44d-1f3fe","👍🏿":"1f44d-1f3ff","👎🏻":"1f44e-1f3fb","👎🏼":"1f44e-1f3fc","👎🏽":"1f44e-1f3fd","👎🏾":"1f44e-1f3fe","👎🏿":"1f44e-1f3ff","✊🏻":"270a-1f3fb","✊🏼":"270a-1f3fc","✊🏽":"270a-1f3fd","✊🏾":"270a-1f3fe","✊🏿":"270a-1f3ff","👊🏻":"1f44a-1f3fb","👊🏼":"1f44a-1f3fc","👊🏽":"1f44a-1f3fd","👊🏾":"1f44a-1f3fe","👊🏿":"1f44a-1f3ff","🤛🏻":"1f91b-1f3fb","🤛🏼":"1f91b-1f3fc","🤛🏽":"1f91b-1f3fd","🤛🏾":"1f91b-1f3fe","🤛🏿":"1f91b-1f3ff","🤜🏻":"1f91c-1f3fb","🤜🏼":"1f91c-1f3fc","🤜🏽":"1f91c-1f3fd","🤜🏾":"1f91c-1f3fe","🤜🏿":"1f91c-1f3ff","🤚🏻":"1f91a-1f3fb","🤚🏼":"1f91a-1f3fc","🤚🏽":"1f91a-1f3fd","🤚🏾":"1f91a-1f3fe","🤚🏿":"1f91a-1f3ff","👋🏻":"1f44b-1f3fb","👋🏼":"1f44b-1f3fc","👋🏽":"1f44b-1f3fd","👋🏾":"1f44b-1f3fe","👋🏿":"1f44b-1f3ff","🤟🏻":"1f91f-1f3fb","🤟🏼":"1f91f-1f3fc","🤟🏽":"1f91f-1f3fd","🤟🏾":"1f91f-1f3fe","🤟🏿":"1f91f-1f3ff","✍️":"270d","✍🏻":"270d-1f3fb","✍🏼":"270d-1f3fc","✍🏽":"270d-1f3fd","✍🏾":"270d-1f3fe","✍🏿":"270d-1f3ff","👏🏻":"1f44f-1f3fb","👏🏼":"1f44f-1f3fc","👏🏽":"1f44f-1f3fd","👏🏾":"1f44f-1f3fe","👏🏿":"1f44f-1f3ff","👐🏻":"1f450-1f3fb","👐🏼":"1f450-1f3fc","👐🏽":"1f450-1f3fd","👐🏾":"1f450-1f3fe","👐🏿":"1f450-1f3ff","🙌🏻":"1f64c-1f3fb","🙌🏼":"1f64c-1f3fc","🙌🏽":"1f64c-1f3fd","🙌🏾":"1f64c-1f3fe","🙌🏿":"1f64c-1f3ff","🤲🏻":"1f932-1f3fb","🤲🏼":"1f932-1f3fc","🤲🏽":"1f932-1f3fd","🤲🏾":"1f932-1f3fe","🤲🏿":"1f932-1f3ff","🙏🏻":"1f64f-1f3fb","🙏🏼":"1f64f-1f3fc","🙏🏽":"1f64f-1f3fd","🙏🏾":"1f64f-1f3fe","🙏🏿":"1f64f-1f3ff","💅🏻":"1f485-1f3fb","💅🏼":"1f485-1f3fc","💅🏽":"1f485-1f3fd","💅🏾":"1f485-1f3fe","💅🏿":"1f485-1f3ff","👂🏻":"1f442-1f3fb","👂🏼":"1f442-1f3fc","👂🏽":"1f442-1f3fd","👂🏾":"1f442-1f3fe","👂🏿":"1f442-1f3ff","👃🏻":"1f443-1f3fb","👃🏼":"1f443-1f3fc","👃🏽":"1f443-1f3fd","👃🏾":"1f443-1f3fe","👃🏿":"1f443-1f3ff","👁️":"1f441","❤️":"2764","❣️":"2763","🗨️":"1f5e8","🗯️":"1f5ef","🕳️":"1f573","🕶️":"1f576","🛍️":"1f6cd","⛑️":"26d1","🐿️":"1f43f","🕊️":"1f54a","🕷️":"1f577","🕸️":"1f578","🏵️":"1f3f5","☘️":"2618","🌶️":"1f336","🍽️":"1f37d","🗺️":"1f5fa","🏔️":"1f3d4","⛰️":"26f0","🏕️":"1f3d5","🏖️":"1f3d6","🏜️":"1f3dc","🏝️":"1f3dd","🏞️":"1f3de","🏟️":"1f3df","🏛️":"1f3db","🏗️":"1f3d7","🏘️":"1f3d8","🏙️":"1f3d9","🏚️":"1f3da","⛩️":"26e9","♨️":"2668","🖼️":"1f5bc","🛣️":"1f6e3","🛤️":"1f6e4","🛳️":"1f6f3","⛴️":"26f4","🛥️":"1f6e5","✈️":"2708","🛩️":"1f6e9","🛰️":"1f6f0","🛎️":"1f6ce","🛏️":"1f6cf","🛋️":"1f6cb","⏱️":"23f1","⏲️":"23f2","🕰️":"1f570","🌡️":"1f321","☀️":"2600","☁️":"2601","⛈️":"26c8","🌤️":"1f324","🌥️":"1f325","🌦️":"1f326","🌧️":"1f327","🌨️":"1f328","🌩️":"1f329","🌪️":"1f32a","🌫️":"1f32b","🌬️":"1f32c","☂️":"2602","⛱️":"26f1","❄️":"2744","☃️":"2603","☄️":"2604","🎗️":"1f397","🎟️":"1f39f","🎖️":"1f396","⛸️":"26f8","🕹️":"1f579","♠️":"2660","♥️":"2665","♦️":"2666","♣️":"2663","🎙️":"1f399","🎚️":"1f39a","🎛️":"1f39b","☎️":"260e","🖥️":"1f5a5","🖨️":"1f5a8","⌨️":"2328","🖱️":"1f5b1","🖲️":"1f5b2","🎞️":"1f39e","📽️":"1f4fd","🕯️":"1f56f","🗞️":"1f5de","🏷️":"1f3f7","✉️":"2709","🗳️":"1f5f3","✏️":"270f","✒️":"2712","🖋️":"1f58b","🖊️":"1f58a","🖌️":"1f58c","🖍️":"1f58d","🗂️":"1f5c2","🗒️":"1f5d2","🗓️":"1f5d3","🖇️":"1f587","✂️":"2702","🗃️":"1f5c3","🗄️":"1f5c4","🗑️":"1f5d1","🗝️":"1f5dd","⛏️":"26cf","⚒️":"2692","🛠️":"1f6e0","🗡️":"1f5e1","⚔️":"2694","🛡️":"1f6e1","⚙️":"2699","🗜️":"1f5dc","⚗️":"2697","⚖️":"2696","⛓️":"26d3","⚰️":"26b0","⚱️":"26b1","🛢️":"1f6e2","⚠️":"26a0","☢️":"2622","☣️":"2623","⬆️":"2b06","↗️":"2197","➡️":"27a1","↘️":"2198","⬇️":"2b07","↙️":"2199","⬅️":"2b05","↖️":"2196","↕️":"2195","↔️":"2194","↩️":"21a9","↪️":"21aa","⤴️":"2934","⤵️":"2935","⚛️":"269b","🕉️":"1f549","✡️":"2721","☸️":"2638","☯️":"262f","✝️":"271d","☦️":"2626","☪️":"262a","☮️":"262e","▶️":"25b6","⏭️":"23ed","⏯️":"23ef","◀️":"25c0","⏮️":"23ee","⏸️":"23f8","⏹️":"23f9","⏺️":"23fa","⏏️":"23cf","♀️":"2640","♂️":"2642","⚕️":"2695","♻️":"267b","⚜️":"269c","☑️":"2611","✔️":"2714","✖️":"2716","〽️":"303d","✳️":"2733","✴️":"2734","❇️":"2747","‼️":"203c","⁉️":"2049","〰️":"3030","©️":"a9","®️":"ae","™️":"2122","#⃣":"23-20e3","*⃣":"2a-20e3","0⃣":"30-20e3","1⃣":"31-20e3","2⃣":"32-20e3","3⃣":"33-20e3","4⃣":"34-20e3","5⃣":"35-20e3","6⃣":"36-20e3","7⃣":"37-20e3","8⃣":"38-20e3","9⃣":"39-20e3","🅰️":"1f170","🅱️":"1f171","ℹ️":"2139","Ⓜ️":"24c2","🅾️":"1f17e","🅿️":"1f17f","🈂️":"1f202","🈷️":"1f237","㊗️":"3297","㊙️":"3299","▪️":"25aa","▫️":"25ab","◻️":"25fb","◼️":"25fc","🏳️":"1f3f3","🇦🇨":"1f1e6-1f1e8","🇦🇩":"1f1e6-1f1e9","🇦🇪":"1f1e6-1f1ea","🇦🇫":"1f1e6-1f1eb","🇦🇬":"1f1e6-1f1ec","🇦🇮":"1f1e6-1f1ee","🇦🇱":"1f1e6-1f1f1","🇦🇲":"1f1e6-1f1f2","🇦🇴":"1f1e6-1f1f4","🇦🇶":"1f1e6-1f1f6","🇦🇷":"1f1e6-1f1f7","🇦🇸":"1f1e6-1f1f8","🇦🇹":"1f1e6-1f1f9","🇦🇺":"1f1e6-1f1fa","🇦🇼":"1f1e6-1f1fc","🇦🇽":"1f1e6-1f1fd","🇦🇿":"1f1e6-1f1ff","🇧🇦":"1f1e7-1f1e6","🇧🇧":"1f1e7-1f1e7","🇧🇩":"1f1e7-1f1e9","🇧🇪":"1f1e7-1f1ea","🇧🇫":"1f1e7-1f1eb","🇧🇬":"1f1e7-1f1ec","🇧🇭":"1f1e7-1f1ed","🇧🇮":"1f1e7-1f1ee","🇧🇯":"1f1e7-1f1ef","🇧🇱":"1f1e7-1f1f1","🇧🇲":"1f1e7-1f1f2","🇧🇳":"1f1e7-1f1f3","🇧🇴":"1f1e7-1f1f4","🇧🇶":"1f1e7-1f1f6","🇧🇷":"1f1e7-1f1f7","🇧🇸":"1f1e7-1f1f8","🇧🇹":"1f1e7-1f1f9","🇧🇻":"1f1e7-1f1fb","🇧🇼":"1f1e7-1f1fc","🇧🇾":"1f1e7-1f1fe","🇧🇿":"1f1e7-1f1ff","🇨🇦":"1f1e8-1f1e6","🇨🇨":"1f1e8-1f1e8","🇨🇩":"1f1e8-1f1e9","🇨🇫":"1f1e8-1f1eb","🇨🇬":"1f1e8-1f1ec","🇨🇭":"1f1e8-1f1ed","🇨🇮":"1f1e8-1f1ee","🇨🇰":"1f1e8-1f1f0","🇨🇱":"1f1e8-1f1f1","🇨🇲":"1f1e8-1f1f2","🇨🇳":"1f1e8-1f1f3","🇨🇴":"1f1e8-1f1f4","🇨🇵":"1f1e8-1f1f5","🇨🇷":"1f1e8-1f1f7","🇨🇺":"1f1e8-1f1fa","🇨🇻":"1f1e8-1f1fb","🇨🇼":"1f1e8-1f1fc","🇨🇽":"1f1e8-1f1fd","🇨🇾":"1f1e8-1f1fe","🇨🇿":"1f1e8-1f1ff","🇩🇪":"1f1e9-1f1ea","🇩🇬":"1f1e9-1f1ec","🇩🇯":"1f1e9-1f1ef","🇩🇰":"1f1e9-1f1f0","🇩🇲":"1f1e9-1f1f2","🇩🇴":"1f1e9-1f1f4","🇩🇿":"1f1e9-1f1ff","🇪🇦":"1f1ea-1f1e6","🇪🇨":"1f1ea-1f1e8","🇪🇪":"1f1ea-1f1ea","🇪🇬":"1f1ea-1f1ec","🇪🇭":"1f1ea-1f1ed","🇪🇷":"1f1ea-1f1f7","🇪🇸":"1f1ea-1f1f8","🇪🇹":"1f1ea-1f1f9","🇪🇺":"1f1ea-1f1fa","🇫🇮":"1f1eb-1f1ee","🇫🇯":"1f1eb-1f1ef","🇫🇰":"1f1eb-1f1f0","🇫🇲":"1f1eb-1f1f2","🇫🇴":"1f1eb-1f1f4","🇫🇷":"1f1eb-1f1f7","🇬🇦":"1f1ec-1f1e6","🇬🇧":"1f1ec-1f1e7","🇬🇩":"1f1ec-1f1e9","🇬🇪":"1f1ec-1f1ea","🇬🇫":"1f1ec-1f1eb","🇬🇬":"1f1ec-1f1ec","🇬🇭":"1f1ec-1f1ed","🇬🇮":"1f1ec-1f1ee","🇬🇱":"1f1ec-1f1f1","🇬🇲":"1f1ec-1f1f2","🇬🇳":"1f1ec-1f1f3","🇬🇵":"1f1ec-1f1f5","🇬🇶":"1f1ec-1f1f6","🇬🇷":"1f1ec-1f1f7","🇬🇸":"1f1ec-1f1f8","🇬🇹":"1f1ec-1f1f9","🇬🇺":"1f1ec-1f1fa","🇬🇼":"1f1ec-1f1fc","🇬🇾":"1f1ec-1f1fe","🇭🇰":"1f1ed-1f1f0","🇭🇲":"1f1ed-1f1f2","🇭🇳":"1f1ed-1f1f3","🇭🇷":"1f1ed-1f1f7","🇭🇹":"1f1ed-1f1f9","🇭🇺":"1f1ed-1f1fa","🇮🇨":"1f1ee-1f1e8","🇮🇩":"1f1ee-1f1e9","🇮🇪":"1f1ee-1f1ea","🇮🇱":"1f1ee-1f1f1","🇮🇲":"1f1ee-1f1f2","🇮🇳":"1f1ee-1f1f3","🇮🇴":"1f1ee-1f1f4","🇮🇶":"1f1ee-1f1f6","🇮🇷":"1f1ee-1f1f7","🇮🇸":"1f1ee-1f1f8","🇮🇹":"1f1ee-1f1f9","🇯🇪":"1f1ef-1f1ea","🇯🇲":"1f1ef-1f1f2","🇯🇴":"1f1ef-1f1f4","🇯🇵":"1f1ef-1f1f5","🇰🇪":"1f1f0-1f1ea","🇰🇬":"1f1f0-1f1ec","🇰🇭":"1f1f0-1f1ed","🇰🇮":"1f1f0-1f1ee","🇰🇲":"1f1f0-1f1f2","🇰🇳":"1f1f0-1f1f3","🇰🇵":"1f1f0-1f1f5","🇰🇷":"1f1f0-1f1f7","🇰🇼":"1f1f0-1f1fc","🇰🇾":"1f1f0-1f1fe","🇰🇿":"1f1f0-1f1ff","🇱🇦":"1f1f1-1f1e6","🇱🇧":"1f1f1-1f1e7","🇱🇨":"1f1f1-1f1e8","🇱🇮":"1f1f1-1f1ee","🇱🇰":"1f1f1-1f1f0","🇱🇷":"1f1f1-1f1f7","🇱🇸":"1f1f1-1f1f8","🇱🇹":"1f1f1-1f1f9","🇱🇺":"1f1f1-1f1fa","🇱🇻":"1f1f1-1f1fb","🇱🇾":"1f1f1-1f1fe","🇲🇦":"1f1f2-1f1e6","🇲🇨":"1f1f2-1f1e8","🇲🇩":"1f1f2-1f1e9","🇲🇪":"1f1f2-1f1ea","🇲🇫":"1f1f2-1f1eb","🇲🇬":"1f1f2-1f1ec","🇲🇭":"1f1f2-1f1ed","🇲🇰":"1f1f2-1f1f0","🇲🇱":"1f1f2-1f1f1","🇲🇲":"1f1f2-1f1f2","🇲🇳":"1f1f2-1f1f3","🇲🇴":"1f1f2-1f1f4","🇲🇵":"1f1f2-1f1f5","🇲🇶":"1f1f2-1f1f6","🇲🇷":"1f1f2-1f1f7","🇲🇸":"1f1f2-1f1f8","🇲🇹":"1f1f2-1f1f9","🇲🇺":"1f1f2-1f1fa","🇲🇻":"1f1f2-1f1fb","🇲🇼":"1f1f2-1f1fc","🇲🇽":"1f1f2-1f1fd","🇲🇾":"1f1f2-1f1fe","🇲🇿":"1f1f2-1f1ff","🇳🇦":"1f1f3-1f1e6","🇳🇨":"1f1f3-1f1e8","🇳🇪":"1f1f3-1f1ea","🇳🇫":"1f1f3-1f1eb","🇳🇬":"1f1f3-1f1ec","🇳🇮":"1f1f3-1f1ee","🇳🇱":"1f1f3-1f1f1","🇳🇴":"1f1f3-1f1f4","🇳🇵":"1f1f3-1f1f5","🇳🇷":"1f1f3-1f1f7","🇳🇺":"1f1f3-1f1fa","🇳🇿":"1f1f3-1f1ff","🇴🇲":"1f1f4-1f1f2","🇵🇦":"1f1f5-1f1e6","🇵🇪":"1f1f5-1f1ea","🇵🇫":"1f1f5-1f1eb","🇵🇬":"1f1f5-1f1ec","🇵🇭":"1f1f5-1f1ed","🇵🇰":"1f1f5-1f1f0","🇵🇱":"1f1f5-1f1f1","🇵🇲":"1f1f5-1f1f2","🇵🇳":"1f1f5-1f1f3","🇵🇷":"1f1f5-1f1f7","🇵🇸":"1f1f5-1f1f8","🇵🇹":"1f1f5-1f1f9","🇵🇼":"1f1f5-1f1fc","🇵🇾":"1f1f5-1f1fe","🇶🇦":"1f1f6-1f1e6","🇷🇪":"1f1f7-1f1ea","🇷🇴":"1f1f7-1f1f4","🇷🇸":"1f1f7-1f1f8","🇷🇺":"1f1f7-1f1fa","🇷🇼":"1f1f7-1f1fc","🇸🇦":"1f1f8-1f1e6","🇸🇧":"1f1f8-1f1e7","🇸🇨":"1f1f8-1f1e8","🇸🇩":"1f1f8-1f1e9","🇸🇪":"1f1f8-1f1ea","🇸🇬":"1f1f8-1f1ec","🇸🇭":"1f1f8-1f1ed","🇸🇮":"1f1f8-1f1ee","🇸🇯":"1f1f8-1f1ef","🇸🇰":"1f1f8-1f1f0","🇸🇱":"1f1f8-1f1f1","🇸🇲":"1f1f8-1f1f2","🇸🇳":"1f1f8-1f1f3","🇸🇴":"1f1f8-1f1f4","🇸🇷":"1f1f8-1f1f7","🇸🇸":"1f1f8-1f1f8","🇸🇹":"1f1f8-1f1f9","🇸🇻":"1f1f8-1f1fb","🇸🇽":"1f1f8-1f1fd","🇸🇾":"1f1f8-1f1fe","🇸🇿":"1f1f8-1f1ff","🇹🇦":"1f1f9-1f1e6","🇹🇨":"1f1f9-1f1e8","🇹🇩":"1f1f9-1f1e9","🇹🇫":"1f1f9-1f1eb","🇹🇬":"1f1f9-1f1ec","🇹🇭":"1f1f9-1f1ed","🇹🇯":"1f1f9-1f1ef","🇹🇰":"1f1f9-1f1f0","🇹🇱":"1f1f9-1f1f1","🇹🇲":"1f1f9-1f1f2","🇹🇳":"1f1f9-1f1f3","🇹🇴":"1f1f9-1f1f4","🇹🇷":"1f1f9-1f1f7","🇹🇹":"1f1f9-1f1f9","🇹🇻":"1f1f9-1f1fb","🇹🇼":"1f1f9-1f1fc","🇹🇿":"1f1f9-1f1ff","🇺🇦":"1f1fa-1f1e6","🇺🇬":"1f1fa-1f1ec","🇺🇲":"1f1fa-1f1f2","🇺🇳":"1f1fa-1f1f3","🇺🇸":"1f1fa-1f1f8","🇺🇾":"1f1fa-1f1fe","🇺🇿":"1f1fa-1f1ff","🇻🇦":"1f1fb-1f1e6","🇻🇨":"1f1fb-1f1e8","🇻🇪":"1f1fb-1f1ea","🇻🇬":"1f1fb-1f1ec","🇻🇮":"1f1fb-1f1ee","🇻🇳":"1f1fb-1f1f3","🇻🇺":"1f1fb-1f1fa","🇼🇫":"1f1fc-1f1eb","🇼🇸":"1f1fc-1f1f8","🇽🇰":"1f1fd-1f1f0","🇾🇪":"1f1fe-1f1ea","🇾🇹":"1f1fe-1f1f9","🇿🇦":"1f1ff-1f1e6","🇿🇲":"1f1ff-1f1f2","🇿🇼":"1f1ff-1f1fc","👨⚕":"1f468-200d-2695-fe0f","👩⚕":"1f469-200d-2695-fe0f","👨🎓":"1f468-200d-1f393","👩🎓":"1f469-200d-1f393","👨🏫":"1f468-200d-1f3eb","👩🏫":"1f469-200d-1f3eb","👨⚖":"1f468-200d-2696-fe0f","👩⚖":"1f469-200d-2696-fe0f","👨🌾":"1f468-200d-1f33e","👩🌾":"1f469-200d-1f33e","👨🍳":"1f468-200d-1f373","👩🍳":"1f469-200d-1f373","👨🔧":"1f468-200d-1f527","👩🔧":"1f469-200d-1f527","👨🏭":"1f468-200d-1f3ed","👩🏭":"1f469-200d-1f3ed","👨💼":"1f468-200d-1f4bc","👩💼":"1f469-200d-1f4bc","👨🔬":"1f468-200d-1f52c","👩🔬":"1f469-200d-1f52c","👨💻":"1f468-200d-1f4bb","👩💻":"1f469-200d-1f4bb","👨🎤":"1f468-200d-1f3a4","👩🎤":"1f469-200d-1f3a4","👨🎨":"1f468-200d-1f3a8","👩🎨":"1f469-200d-1f3a8","👨✈":"1f468-200d-2708-fe0f","👩✈":"1f469-200d-2708-fe0f","👨🚀":"1f468-200d-1f680","👩🚀":"1f469-200d-1f680","👨🚒":"1f468-200d-1f692","👩🚒":"1f469-200d-1f692","👮♂":"1f46e-200d-2642-fe0f","👮♀":"1f46e-200d-2640-fe0f","🕵♂":"1f575-fe0f-200d-2642-fe0f","🕵♀":"1f575-fe0f-200d-2640-fe0f","💂♂":"1f482-200d-2642-fe0f","💂♀":"1f482-200d-2640-fe0f","👷♂":"1f477-200d-2642-fe0f","👷♀":"1f477-200d-2640-fe0f","👳♂":"1f473-200d-2642-fe0f","👳♀":"1f473-200d-2640-fe0f","👱♂":"1f471-200d-2642-fe0f","👱♀":"1f471-200d-2640-fe0f","🧙♀":"1f9d9-200d-2640-fe0f","🧙♂":"1f9d9-200d-2642-fe0f","🧚♀":"1f9da-200d-2640-fe0f","🧚♂":"1f9da-200d-2642-fe0f","🧛♀":"1f9db-200d-2640-fe0f","🧛♂":"1f9db-200d-2642-fe0f","🧜♀":"1f9dc-200d-2640-fe0f","🧜♂":"1f9dc-200d-2642-fe0f","🧝♀":"1f9dd-200d-2640-fe0f","🧝♂":"1f9dd-200d-2642-fe0f","🧞♀":"1f9de-200d-2640-fe0f","🧞♂":"1f9de-200d-2642-fe0f","🧟♀":"1f9df-200d-2640-fe0f","🧟♂":"1f9df-200d-2642-fe0f","🙍♂":"1f64d-200d-2642-fe0f","🙍♀":"1f64d-200d-2640-fe0f","🙎♂":"1f64e-200d-2642-fe0f","🙎♀":"1f64e-200d-2640-fe0f","🙅♂":"1f645-200d-2642-fe0f","🙅♀":"1f645-200d-2640-fe0f","🙆♂":"1f646-200d-2642-fe0f","🙆♀":"1f646-200d-2640-fe0f","💁♂":"1f481-200d-2642-fe0f","💁♀":"1f481-200d-2640-fe0f","🙋♂":"1f64b-200d-2642-fe0f","🙋♀":"1f64b-200d-2640-fe0f","🙇♂":"1f647-200d-2642-fe0f","🙇♀":"1f647-200d-2640-fe0f","🤦♂":"1f926-200d-2642-fe0f","🤦♀":"1f926-200d-2640-fe0f","🤷♂":"1f937-200d-2642-fe0f","🤷♀":"1f937-200d-2640-fe0f","💆♂":"1f486-200d-2642-fe0f","💆♀":"1f486-200d-2640-fe0f","💇♂":"1f487-200d-2642-fe0f","💇♀":"1f487-200d-2640-fe0f","🚶♂":"1f6b6-200d-2642-fe0f","🚶♀":"1f6b6-200d-2640-fe0f","🏃♂":"1f3c3-200d-2642-fe0f","🏃♀":"1f3c3-200d-2640-fe0f","👯♂":"1f46f-200d-2642-fe0f","👯♀":"1f46f-200d-2640-fe0f","🧖♀":"1f9d6-200d-2640-fe0f","🧖♂":"1f9d6-200d-2642-fe0f","🧗♀":"1f9d7-200d-2640-fe0f","🧗♂":"1f9d7-200d-2642-fe0f","🧘♀":"1f9d8-200d-2640-fe0f","🧘♂":"1f9d8-200d-2642-fe0f","🏌♂":"1f3cc-fe0f-200d-2642-fe0f","🏌♀":"1f3cc-fe0f-200d-2640-fe0f","🏄♂":"1f3c4-200d-2642-fe0f","🏄♀":"1f3c4-200d-2640-fe0f","🚣♂":"1f6a3-200d-2642-fe0f","🚣♀":"1f6a3-200d-2640-fe0f","🏊♂":"1f3ca-200d-2642-fe0f","🏊♀":"1f3ca-200d-2640-fe0f","⛹♂":"26f9-fe0f-200d-2642-fe0f","⛹♀":"26f9-fe0f-200d-2640-fe0f","🏋♂":"1f3cb-fe0f-200d-2642-fe0f","🏋♀":"1f3cb-fe0f-200d-2640-fe0f","🚴♂":"1f6b4-200d-2642-fe0f","🚴♀":"1f6b4-200d-2640-fe0f","🚵♂":"1f6b5-200d-2642-fe0f","🚵♀":"1f6b5-200d-2640-fe0f","🤸♂":"1f938-200d-2642-fe0f","🤸♀":"1f938-200d-2640-fe0f","🤼♂":"1f93c-200d-2642-fe0f","🤼♀":"1f93c-200d-2640-fe0f","🤽♂":"1f93d-200d-2642-fe0f","🤽♀":"1f93d-200d-2640-fe0f","🤾♂":"1f93e-200d-2642-fe0f","🤾♀":"1f93e-200d-2640-fe0f","🤹♂":"1f939-200d-2642-fe0f","🤹♀":"1f939-200d-2640-fe0f","👨👦":"1f468-200d-1f466","👨👧":"1f468-200d-1f467","👩👦":"1f469-200d-1f466","👩👧":"1f469-200d-1f467","👁🗨":"1f441-200d-1f5e8","#️⃣":"23-20e3","*️⃣":"2a-20e3","0️⃣":"30-20e3","1️⃣":"31-20e3","2️⃣":"32-20e3","3️⃣":"33-20e3","4️⃣":"34-20e3","5️⃣":"35-20e3","6️⃣":"36-20e3","7️⃣":"37-20e3","8️⃣":"38-20e3","9️⃣":"39-20e3","🏳🌈":"1f3f3-fe0f-200d-1f308","👨⚕️":"1f468-200d-2695-fe0f","👨🏻⚕":"1f468-1f3fb-200d-2695-fe0f","👨🏼⚕":"1f468-1f3fc-200d-2695-fe0f","👨🏽⚕":"1f468-1f3fd-200d-2695-fe0f","👨🏾⚕":"1f468-1f3fe-200d-2695-fe0f","👨🏿⚕":"1f468-1f3ff-200d-2695-fe0f","👩⚕️":"1f469-200d-2695-fe0f","👩🏻⚕":"1f469-1f3fb-200d-2695-fe0f","👩🏼⚕":"1f469-1f3fc-200d-2695-fe0f","👩🏽⚕":"1f469-1f3fd-200d-2695-fe0f","👩🏾⚕":"1f469-1f3fe-200d-2695-fe0f","👩🏿⚕":"1f469-1f3ff-200d-2695-fe0f","👨🏻🎓":"1f468-1f3fb-200d-1f393","👨🏼🎓":"1f468-1f3fc-200d-1f393","👨🏽🎓":"1f468-1f3fd-200d-1f393","👨🏾🎓":"1f468-1f3fe-200d-1f393","👨🏿🎓":"1f468-1f3ff-200d-1f393","👩🏻🎓":"1f469-1f3fb-200d-1f393","👩🏼🎓":"1f469-1f3fc-200d-1f393","👩🏽🎓":"1f469-1f3fd-200d-1f393","👩🏾🎓":"1f469-1f3fe-200d-1f393","👩🏿🎓":"1f469-1f3ff-200d-1f393","👨🏻🏫":"1f468-1f3fb-200d-1f3eb","👨🏼🏫":"1f468-1f3fc-200d-1f3eb","👨🏽🏫":"1f468-1f3fd-200d-1f3eb","👨🏾🏫":"1f468-1f3fe-200d-1f3eb","👨🏿🏫":"1f468-1f3ff-200d-1f3eb","👩🏻🏫":"1f469-1f3fb-200d-1f3eb","👩🏼🏫":"1f469-1f3fc-200d-1f3eb","👩🏽🏫":"1f469-1f3fd-200d-1f3eb","👩🏾🏫":"1f469-1f3fe-200d-1f3eb","👩🏿🏫":"1f469-1f3ff-200d-1f3eb","👨⚖️":"1f468-200d-2696-fe0f","👨🏻⚖":"1f468-1f3fb-200d-2696-fe0f","👨🏼⚖":"1f468-1f3fc-200d-2696-fe0f","👨🏽⚖":"1f468-1f3fd-200d-2696-fe0f","👨🏾⚖":"1f468-1f3fe-200d-2696-fe0f","👨🏿⚖":"1f468-1f3ff-200d-2696-fe0f","👩⚖️":"1f469-200d-2696-fe0f","👩🏻⚖":"1f469-1f3fb-200d-2696-fe0f","👩🏼⚖":"1f469-1f3fc-200d-2696-fe0f","👩🏽⚖":"1f469-1f3fd-200d-2696-fe0f","👩🏾⚖":"1f469-1f3fe-200d-2696-fe0f","👩🏿⚖":"1f469-1f3ff-200d-2696-fe0f","👨🏻🌾":"1f468-1f3fb-200d-1f33e","👨🏼🌾":"1f468-1f3fc-200d-1f33e","👨🏽🌾":"1f468-1f3fd-200d-1f33e","👨🏾🌾":"1f468-1f3fe-200d-1f33e","👨🏿🌾":"1f468-1f3ff-200d-1f33e","👩🏻🌾":"1f469-1f3fb-200d-1f33e","👩🏼🌾":"1f469-1f3fc-200d-1f33e","👩🏽🌾":"1f469-1f3fd-200d-1f33e","👩🏾🌾":"1f469-1f3fe-200d-1f33e","👩🏿🌾":"1f469-1f3ff-200d-1f33e","👨🏻🍳":"1f468-1f3fb-200d-1f373","👨🏼🍳":"1f468-1f3fc-200d-1f373","👨🏽🍳":"1f468-1f3fd-200d-1f373","👨🏾🍳":"1f468-1f3fe-200d-1f373","👨🏿🍳":"1f468-1f3ff-200d-1f373","👩🏻🍳":"1f469-1f3fb-200d-1f373","👩🏼🍳":"1f469-1f3fc-200d-1f373","👩🏽🍳":"1f469-1f3fd-200d-1f373","👩🏾🍳":"1f469-1f3fe-200d-1f373","👩🏿🍳":"1f469-1f3ff-200d-1f373","👨🏻🔧":"1f468-1f3fb-200d-1f527","👨🏼🔧":"1f468-1f3fc-200d-1f527","👨🏽🔧":"1f468-1f3fd-200d-1f527","👨🏾🔧":"1f468-1f3fe-200d-1f527","👨🏿🔧":"1f468-1f3ff-200d-1f527","👩🏻🔧":"1f469-1f3fb-200d-1f527","👩🏼🔧":"1f469-1f3fc-200d-1f527","👩🏽🔧":"1f469-1f3fd-200d-1f527","👩🏾🔧":"1f469-1f3fe-200d-1f527","👩🏿🔧":"1f469-1f3ff-200d-1f527","👨🏻🏭":"1f468-1f3fb-200d-1f3ed","👨🏼🏭":"1f468-1f3fc-200d-1f3ed","👨🏽🏭":"1f468-1f3fd-200d-1f3ed","👨🏾🏭":"1f468-1f3fe-200d-1f3ed","👨🏿🏭":"1f468-1f3ff-200d-1f3ed","👩🏻🏭":"1f469-1f3fb-200d-1f3ed","👩🏼🏭":"1f469-1f3fc-200d-1f3ed","👩🏽🏭":"1f469-1f3fd-200d-1f3ed","👩🏾🏭":"1f469-1f3fe-200d-1f3ed","👩🏿🏭":"1f469-1f3ff-200d-1f3ed","👨🏻💼":"1f468-1f3fb-200d-1f4bc","👨🏼💼":"1f468-1f3fc-200d-1f4bc","👨🏽💼":"1f468-1f3fd-200d-1f4bc","👨🏾💼":"1f468-1f3fe-200d-1f4bc","👨🏿💼":"1f468-1f3ff-200d-1f4bc","👩🏻💼":"1f469-1f3fb-200d-1f4bc","👩🏼💼":"1f469-1f3fc-200d-1f4bc","👩🏽💼":"1f469-1f3fd-200d-1f4bc","👩🏾💼":"1f469-1f3fe-200d-1f4bc","👩🏿💼":"1f469-1f3ff-200d-1f4bc","👨🏻🔬":"1f468-1f3fb-200d-1f52c","👨🏼🔬":"1f468-1f3fc-200d-1f52c","👨🏽🔬":"1f468-1f3fd-200d-1f52c","👨🏾🔬":"1f468-1f3fe-200d-1f52c","👨🏿🔬":"1f468-1f3ff-200d-1f52c","👩🏻🔬":"1f469-1f3fb-200d-1f52c","👩🏼🔬":"1f469-1f3fc-200d-1f52c","👩🏽🔬":"1f469-1f3fd-200d-1f52c","👩🏾🔬":"1f469-1f3fe-200d-1f52c","👩🏿🔬":"1f469-1f3ff-200d-1f52c","👨🏻💻":"1f468-1f3fb-200d-1f4bb","👨🏼💻":"1f468-1f3fc-200d-1f4bb","👨🏽💻":"1f468-1f3fd-200d-1f4bb","👨🏾💻":"1f468-1f3fe-200d-1f4bb","👨🏿💻":"1f468-1f3ff-200d-1f4bb","👩🏻💻":"1f469-1f3fb-200d-1f4bb","👩🏼💻":"1f469-1f3fc-200d-1f4bb","👩🏽💻":"1f469-1f3fd-200d-1f4bb","👩🏾💻":"1f469-1f3fe-200d-1f4bb","👩🏿💻":"1f469-1f3ff-200d-1f4bb","👨🏻🎤":"1f468-1f3fb-200d-1f3a4","👨🏼🎤":"1f468-1f3fc-200d-1f3a4","👨🏽🎤":"1f468-1f3fd-200d-1f3a4","👨🏾🎤":"1f468-1f3fe-200d-1f3a4","👨🏿🎤":"1f468-1f3ff-200d-1f3a4","👩🏻🎤":"1f469-1f3fb-200d-1f3a4","👩🏼🎤":"1f469-1f3fc-200d-1f3a4","👩🏽🎤":"1f469-1f3fd-200d-1f3a4","👩🏾🎤":"1f469-1f3fe-200d-1f3a4","👩🏿🎤":"1f469-1f3ff-200d-1f3a4","👨🏻🎨":"1f468-1f3fb-200d-1f3a8","👨🏼🎨":"1f468-1f3fc-200d-1f3a8","👨🏽🎨":"1f468-1f3fd-200d-1f3a8","👨🏾🎨":"1f468-1f3fe-200d-1f3a8","👨🏿🎨":"1f468-1f3ff-200d-1f3a8","👩🏻🎨":"1f469-1f3fb-200d-1f3a8","👩🏼🎨":"1f469-1f3fc-200d-1f3a8","👩🏽🎨":"1f469-1f3fd-200d-1f3a8","👩🏾🎨":"1f469-1f3fe-200d-1f3a8","👩🏿🎨":"1f469-1f3ff-200d-1f3a8","👨✈️":"1f468-200d-2708-fe0f","👨🏻✈":"1f468-1f3fb-200d-2708-fe0f","👨🏼✈":"1f468-1f3fc-200d-2708-fe0f","👨🏽✈":"1f468-1f3fd-200d-2708-fe0f","👨🏾✈":"1f468-1f3fe-200d-2708-fe0f","👨🏿✈":"1f468-1f3ff-200d-2708-fe0f","👩✈️":"1f469-200d-2708-fe0f","👩🏻✈":"1f469-1f3fb-200d-2708-fe0f","👩🏼✈":"1f469-1f3fc-200d-2708-fe0f","👩🏽✈":"1f469-1f3fd-200d-2708-fe0f","👩🏾✈":"1f469-1f3fe-200d-2708-fe0f","👩🏿✈":"1f469-1f3ff-200d-2708-fe0f","👨🏻🚀":"1f468-1f3fb-200d-1f680","👨🏼🚀":"1f468-1f3fc-200d-1f680","👨🏽🚀":"1f468-1f3fd-200d-1f680","👨🏾🚀":"1f468-1f3fe-200d-1f680","👨🏿🚀":"1f468-1f3ff-200d-1f680","👩🏻🚀":"1f469-1f3fb-200d-1f680","👩🏼🚀":"1f469-1f3fc-200d-1f680","👩🏽🚀":"1f469-1f3fd-200d-1f680","👩🏾🚀":"1f469-1f3fe-200d-1f680","👩🏿🚀":"1f469-1f3ff-200d-1f680","👨🏻🚒":"1f468-1f3fb-200d-1f692","👨🏼🚒":"1f468-1f3fc-200d-1f692","👨🏽🚒":"1f468-1f3fd-200d-1f692","👨🏾🚒":"1f468-1f3fe-200d-1f692","👨🏿🚒":"1f468-1f3ff-200d-1f692","👩🏻🚒":"1f469-1f3fb-200d-1f692","👩🏼🚒":"1f469-1f3fc-200d-1f692","👩🏽🚒":"1f469-1f3fd-200d-1f692","👩🏾🚒":"1f469-1f3fe-200d-1f692","👩🏿🚒":"1f469-1f3ff-200d-1f692","👮♂️":"1f46e-200d-2642-fe0f","👮🏻♂":"1f46e-1f3fb-200d-2642-fe0f","👮🏼♂":"1f46e-1f3fc-200d-2642-fe0f","👮🏽♂":"1f46e-1f3fd-200d-2642-fe0f","👮🏾♂":"1f46e-1f3fe-200d-2642-fe0f","👮🏿♂":"1f46e-1f3ff-200d-2642-fe0f","👮♀️":"1f46e-200d-2640-fe0f","👮🏻♀":"1f46e-1f3fb-200d-2640-fe0f","👮🏼♀":"1f46e-1f3fc-200d-2640-fe0f","👮🏽♀":"1f46e-1f3fd-200d-2640-fe0f","👮🏾♀":"1f46e-1f3fe-200d-2640-fe0f","👮🏿♀":"1f46e-1f3ff-200d-2640-fe0f","🕵♂️":"1f575-fe0f-200d-2642-fe0f","🕵️♂":"1f575-fe0f-200d-2642-fe0f","🕵🏻♂":"1f575-1f3fb-200d-2642-fe0f","🕵🏼♂":"1f575-1f3fc-200d-2642-fe0f","🕵🏽♂":"1f575-1f3fd-200d-2642-fe0f","🕵🏾♂":"1f575-1f3fe-200d-2642-fe0f","🕵🏿♂":"1f575-1f3ff-200d-2642-fe0f","🕵♀️":"1f575-fe0f-200d-2640-fe0f","🕵️♀":"1f575-fe0f-200d-2640-fe0f","🕵🏻♀":"1f575-1f3fb-200d-2640-fe0f","🕵🏼♀":"1f575-1f3fc-200d-2640-fe0f","🕵🏽♀":"1f575-1f3fd-200d-2640-fe0f","🕵🏾♀":"1f575-1f3fe-200d-2640-fe0f","🕵🏿♀":"1f575-1f3ff-200d-2640-fe0f","💂♂️":"1f482-200d-2642-fe0f","💂🏻♂":"1f482-1f3fb-200d-2642-fe0f","💂🏼♂":"1f482-1f3fc-200d-2642-fe0f","💂🏽♂":"1f482-1f3fd-200d-2642-fe0f","💂🏾♂":"1f482-1f3fe-200d-2642-fe0f","💂🏿♂":"1f482-1f3ff-200d-2642-fe0f","💂♀️":"1f482-200d-2640-fe0f","💂🏻♀":"1f482-1f3fb-200d-2640-fe0f","💂🏼♀":"1f482-1f3fc-200d-2640-fe0f","💂🏽♀":"1f482-1f3fd-200d-2640-fe0f","💂🏾♀":"1f482-1f3fe-200d-2640-fe0f","💂🏿♀":"1f482-1f3ff-200d-2640-fe0f","👷♂️":"1f477-200d-2642-fe0f","👷🏻♂":"1f477-1f3fb-200d-2642-fe0f","👷🏼♂":"1f477-1f3fc-200d-2642-fe0f","👷🏽♂":"1f477-1f3fd-200d-2642-fe0f","👷🏾♂":"1f477-1f3fe-200d-2642-fe0f","👷🏿♂":"1f477-1f3ff-200d-2642-fe0f","👷♀️":"1f477-200d-2640-fe0f","👷🏻♀":"1f477-1f3fb-200d-2640-fe0f","👷🏼♀":"1f477-1f3fc-200d-2640-fe0f","👷🏽♀":"1f477-1f3fd-200d-2640-fe0f","👷🏾♀":"1f477-1f3fe-200d-2640-fe0f","👷🏿♀":"1f477-1f3ff-200d-2640-fe0f","👳♂️":"1f473-200d-2642-fe0f","👳🏻♂":"1f473-1f3fb-200d-2642-fe0f","👳🏼♂":"1f473-1f3fc-200d-2642-fe0f","👳🏽♂":"1f473-1f3fd-200d-2642-fe0f","👳🏾♂":"1f473-1f3fe-200d-2642-fe0f","👳🏿♂":"1f473-1f3ff-200d-2642-fe0f","👳♀️":"1f473-200d-2640-fe0f","👳🏻♀":"1f473-1f3fb-200d-2640-fe0f","👳🏼♀":"1f473-1f3fc-200d-2640-fe0f","👳🏽♀":"1f473-1f3fd-200d-2640-fe0f","👳🏾♀":"1f473-1f3fe-200d-2640-fe0f","👳🏿♀":"1f473-1f3ff-200d-2640-fe0f","👱♂️":"1f471-200d-2642-fe0f","👱🏻♂":"1f471-1f3fb-200d-2642-fe0f","👱🏼♂":"1f471-1f3fc-200d-2642-fe0f","👱🏽♂":"1f471-1f3fd-200d-2642-fe0f","👱🏾♂":"1f471-1f3fe-200d-2642-fe0f","👱🏿♂":"1f471-1f3ff-200d-2642-fe0f","👱♀️":"1f471-200d-2640-fe0f","👱🏻♀":"1f471-1f3fb-200d-2640-fe0f","👱🏼♀":"1f471-1f3fc-200d-2640-fe0f","👱🏽♀":"1f471-1f3fd-200d-2640-fe0f","👱🏾♀":"1f471-1f3fe-200d-2640-fe0f","👱🏿♀":"1f471-1f3ff-200d-2640-fe0f","🧙♀️":"1f9d9-200d-2640-fe0f","🧙🏻♀":"1f9d9-1f3fb-200d-2640-fe0f","🧙🏼♀":"1f9d9-1f3fc-200d-2640-fe0f","🧙🏽♀":"1f9d9-1f3fd-200d-2640-fe0f","🧙🏾♀":"1f9d9-1f3fe-200d-2640-fe0f","🧙🏿♀":"1f9d9-1f3ff-200d-2640-fe0f","🧙♂️":"1f9d9-200d-2642-fe0f","🧙🏻♂":"1f9d9-1f3fb-200d-2642-fe0f","🧙🏼♂":"1f9d9-1f3fc-200d-2642-fe0f","🧙🏽♂":"1f9d9-1f3fd-200d-2642-fe0f","🧙🏾♂":"1f9d9-1f3fe-200d-2642-fe0f","🧙🏿♂":"1f9d9-1f3ff-200d-2642-fe0f","🧚♀️":"1f9da-200d-2640-fe0f","🧚🏻♀":"1f9da-1f3fb-200d-2640-fe0f","🧚🏼♀":"1f9da-1f3fc-200d-2640-fe0f","🧚🏽♀":"1f9da-1f3fd-200d-2640-fe0f","🧚🏾♀":"1f9da-1f3fe-200d-2640-fe0f","🧚🏿♀":"1f9da-1f3ff-200d-2640-fe0f","🧚♂️":"1f9da-200d-2642-fe0f","🧚🏻♂":"1f9da-1f3fb-200d-2642-fe0f","🧚🏼♂":"1f9da-1f3fc-200d-2642-fe0f","🧚🏽♂":"1f9da-1f3fd-200d-2642-fe0f","🧚🏾♂":"1f9da-1f3fe-200d-2642-fe0f","🧚🏿♂":"1f9da-1f3ff-200d-2642-fe0f","🧛♀️":"1f9db-200d-2640-fe0f","🧛🏻♀":"1f9db-1f3fb-200d-2640-fe0f","🧛🏼♀":"1f9db-1f3fc-200d-2640-fe0f","🧛🏽♀":"1f9db-1f3fd-200d-2640-fe0f","🧛🏾♀":"1f9db-1f3fe-200d-2640-fe0f","🧛🏿♀":"1f9db-1f3ff-200d-2640-fe0f","🧛♂️":"1f9db-200d-2642-fe0f","🧛🏻♂":"1f9db-1f3fb-200d-2642-fe0f","🧛🏼♂":"1f9db-1f3fc-200d-2642-fe0f","🧛🏽♂":"1f9db-1f3fd-200d-2642-fe0f","🧛🏾♂":"1f9db-1f3fe-200d-2642-fe0f","🧛🏿♂":"1f9db-1f3ff-200d-2642-fe0f","🧜♀️":"1f9dc-200d-2640-fe0f","🧜🏻♀":"1f9dc-1f3fb-200d-2640-fe0f","🧜🏼♀":"1f9dc-1f3fc-200d-2640-fe0f","🧜🏽♀":"1f9dc-1f3fd-200d-2640-fe0f","🧜🏾♀":"1f9dc-1f3fe-200d-2640-fe0f","🧜🏿♀":"1f9dc-1f3ff-200d-2640-fe0f","🧜♂️":"1f9dc-200d-2642-fe0f","🧜🏻♂":"1f9dc-1f3fb-200d-2642-fe0f","🧜🏼♂":"1f9dc-1f3fc-200d-2642-fe0f","🧜🏽♂":"1f9dc-1f3fd-200d-2642-fe0f","🧜🏾♂":"1f9dc-1f3fe-200d-2642-fe0f","🧜🏿♂":"1f9dc-1f3ff-200d-2642-fe0f","🧝♀️":"1f9dd-200d-2640-fe0f","🧝🏻♀":"1f9dd-1f3fb-200d-2640-fe0f","🧝🏼♀":"1f9dd-1f3fc-200d-2640-fe0f","🧝🏽♀":"1f9dd-1f3fd-200d-2640-fe0f","🧝🏾♀":"1f9dd-1f3fe-200d-2640-fe0f","🧝🏿♀":"1f9dd-1f3ff-200d-2640-fe0f","🧝♂️":"1f9dd-200d-2642-fe0f","🧝🏻♂":"1f9dd-1f3fb-200d-2642-fe0f","🧝🏼♂":"1f9dd-1f3fc-200d-2642-fe0f","🧝🏽♂":"1f9dd-1f3fd-200d-2642-fe0f","🧝🏾♂":"1f9dd-1f3fe-200d-2642-fe0f","🧝🏿♂":"1f9dd-1f3ff-200d-2642-fe0f","🧞♀️":"1f9de-200d-2640-fe0f","🧞♂️":"1f9de-200d-2642-fe0f","🧟♀️":"1f9df-200d-2640-fe0f","🧟♂️":"1f9df-200d-2642-fe0f","🙍♂️":"1f64d-200d-2642-fe0f","🙍🏻♂":"1f64d-1f3fb-200d-2642-fe0f","🙍🏼♂":"1f64d-1f3fc-200d-2642-fe0f","🙍🏽♂":"1f64d-1f3fd-200d-2642-fe0f","🙍🏾♂":"1f64d-1f3fe-200d-2642-fe0f","🙍🏿♂":"1f64d-1f3ff-200d-2642-fe0f","🙍♀️":"1f64d-200d-2640-fe0f","🙍🏻♀":"1f64d-1f3fb-200d-2640-fe0f","🙍🏼♀":"1f64d-1f3fc-200d-2640-fe0f","🙍🏽♀":"1f64d-1f3fd-200d-2640-fe0f","🙍🏾♀":"1f64d-1f3fe-200d-2640-fe0f","🙍🏿♀":"1f64d-1f3ff-200d-2640-fe0f","🙎♂️":"1f64e-200d-2642-fe0f","🙎🏻♂":"1f64e-1f3fb-200d-2642-fe0f","🙎🏼♂":"1f64e-1f3fc-200d-2642-fe0f","🙎🏽♂":"1f64e-1f3fd-200d-2642-fe0f","🙎🏾♂":"1f64e-1f3fe-200d-2642-fe0f","🙎🏿♂":"1f64e-1f3ff-200d-2642-fe0f","🙎♀️":"1f64e-200d-2640-fe0f","🙎🏻♀":"1f64e-1f3fb-200d-2640-fe0f","🙎🏼♀":"1f64e-1f3fc-200d-2640-fe0f","🙎🏽♀":"1f64e-1f3fd-200d-2640-fe0f","🙎🏾♀":"1f64e-1f3fe-200d-2640-fe0f","🙎🏿♀":"1f64e-1f3ff-200d-2640-fe0f","🙅♂️":"1f645-200d-2642-fe0f","🙅🏻♂":"1f645-1f3fb-200d-2642-fe0f","🙅🏼♂":"1f645-1f3fc-200d-2642-fe0f","🙅🏽♂":"1f645-1f3fd-200d-2642-fe0f","🙅🏾♂":"1f645-1f3fe-200d-2642-fe0f","🙅🏿♂":"1f645-1f3ff-200d-2642-fe0f","🙅♀️":"1f645-200d-2640-fe0f","🙅🏻♀":"1f645-1f3fb-200d-2640-fe0f","🙅🏼♀":"1f645-1f3fc-200d-2640-fe0f","🙅🏽♀":"1f645-1f3fd-200d-2640-fe0f","🙅🏾♀":"1f645-1f3fe-200d-2640-fe0f","🙅🏿♀":"1f645-1f3ff-200d-2640-fe0f","🙆♂️":"1f646-200d-2642-fe0f","🙆🏻♂":"1f646-1f3fb-200d-2642-fe0f","🙆🏼♂":"1f646-1f3fc-200d-2642-fe0f","🙆🏽♂":"1f646-1f3fd-200d-2642-fe0f","🙆🏾♂":"1f646-1f3fe-200d-2642-fe0f","🙆🏿♂":"1f646-1f3ff-200d-2642-fe0f","🙆♀️":"1f646-200d-2640-fe0f","🙆🏻♀":"1f646-1f3fb-200d-2640-fe0f","🙆🏼♀":"1f646-1f3fc-200d-2640-fe0f","🙆🏽♀":"1f646-1f3fd-200d-2640-fe0f","🙆🏾♀":"1f646-1f3fe-200d-2640-fe0f","🙆🏿♀":"1f646-1f3ff-200d-2640-fe0f","💁♂️":"1f481-200d-2642-fe0f","💁🏻♂":"1f481-1f3fb-200d-2642-fe0f","💁🏼♂":"1f481-1f3fc-200d-2642-fe0f","💁🏽♂":"1f481-1f3fd-200d-2642-fe0f","💁🏾♂":"1f481-1f3fe-200d-2642-fe0f","💁🏿♂":"1f481-1f3ff-200d-2642-fe0f","💁♀️":"1f481-200d-2640-fe0f","💁🏻♀":"1f481-1f3fb-200d-2640-fe0f","💁🏼♀":"1f481-1f3fc-200d-2640-fe0f","💁🏽♀":"1f481-1f3fd-200d-2640-fe0f","💁🏾♀":"1f481-1f3fe-200d-2640-fe0f","💁🏿♀":"1f481-1f3ff-200d-2640-fe0f","🙋♂️":"1f64b-200d-2642-fe0f","🙋🏻♂":"1f64b-1f3fb-200d-2642-fe0f","🙋🏼♂":"1f64b-1f3fc-200d-2642-fe0f","🙋🏽♂":"1f64b-1f3fd-200d-2642-fe0f","🙋🏾♂":"1f64b-1f3fe-200d-2642-fe0f","🙋🏿♂":"1f64b-1f3ff-200d-2642-fe0f","🙋♀️":"1f64b-200d-2640-fe0f","🙋🏻♀":"1f64b-1f3fb-200d-2640-fe0f","🙋🏼♀":"1f64b-1f3fc-200d-2640-fe0f","🙋🏽♀":"1f64b-1f3fd-200d-2640-fe0f","🙋🏾♀":"1f64b-1f3fe-200d-2640-fe0f","🙋🏿♀":"1f64b-1f3ff-200d-2640-fe0f","🙇♂️":"1f647-200d-2642-fe0f","🙇🏻♂":"1f647-1f3fb-200d-2642-fe0f","🙇🏼♂":"1f647-1f3fc-200d-2642-fe0f","🙇🏽♂":"1f647-1f3fd-200d-2642-fe0f","🙇🏾♂":"1f647-1f3fe-200d-2642-fe0f","🙇🏿♂":"1f647-1f3ff-200d-2642-fe0f","🙇♀️":"1f647-200d-2640-fe0f","🙇🏻♀":"1f647-1f3fb-200d-2640-fe0f","🙇🏼♀":"1f647-1f3fc-200d-2640-fe0f","🙇🏽♀":"1f647-1f3fd-200d-2640-fe0f","🙇🏾♀":"1f647-1f3fe-200d-2640-fe0f","🙇🏿♀":"1f647-1f3ff-200d-2640-fe0f","🤦♂️":"1f926-200d-2642-fe0f","🤦🏻♂":"1f926-1f3fb-200d-2642-fe0f","🤦🏼♂":"1f926-1f3fc-200d-2642-fe0f","🤦🏽♂":"1f926-1f3fd-200d-2642-fe0f","🤦🏾♂":"1f926-1f3fe-200d-2642-fe0f","🤦🏿♂":"1f926-1f3ff-200d-2642-fe0f","🤦♀️":"1f926-200d-2640-fe0f","🤦🏻♀":"1f926-1f3fb-200d-2640-fe0f","🤦🏼♀":"1f926-1f3fc-200d-2640-fe0f","🤦🏽♀":"1f926-1f3fd-200d-2640-fe0f","🤦🏾♀":"1f926-1f3fe-200d-2640-fe0f","🤦🏿♀":"1f926-1f3ff-200d-2640-fe0f","🤷♂️":"1f937-200d-2642-fe0f","🤷🏻♂":"1f937-1f3fb-200d-2642-fe0f","🤷🏼♂":"1f937-1f3fc-200d-2642-fe0f","🤷🏽♂":"1f937-1f3fd-200d-2642-fe0f","🤷🏾♂":"1f937-1f3fe-200d-2642-fe0f","🤷🏿♂":"1f937-1f3ff-200d-2642-fe0f","🤷♀️":"1f937-200d-2640-fe0f","🤷🏻♀":"1f937-1f3fb-200d-2640-fe0f","🤷🏼♀":"1f937-1f3fc-200d-2640-fe0f","🤷🏽♀":"1f937-1f3fd-200d-2640-fe0f","🤷🏾♀":"1f937-1f3fe-200d-2640-fe0f","🤷🏿♀":"1f937-1f3ff-200d-2640-fe0f","💆♂️":"1f486-200d-2642-fe0f","💆🏻♂":"1f486-1f3fb-200d-2642-fe0f","💆🏼♂":"1f486-1f3fc-200d-2642-fe0f","💆🏽♂":"1f486-1f3fd-200d-2642-fe0f","💆🏾♂":"1f486-1f3fe-200d-2642-fe0f","💆🏿♂":"1f486-1f3ff-200d-2642-fe0f","💆♀️":"1f486-200d-2640-fe0f","💆🏻♀":"1f486-1f3fb-200d-2640-fe0f","💆🏼♀":"1f486-1f3fc-200d-2640-fe0f","💆🏽♀":"1f486-1f3fd-200d-2640-fe0f","💆🏾♀":"1f486-1f3fe-200d-2640-fe0f","💆🏿♀":"1f486-1f3ff-200d-2640-fe0f","💇♂️":"1f487-200d-2642-fe0f","💇🏻♂":"1f487-1f3fb-200d-2642-fe0f","💇🏼♂":"1f487-1f3fc-200d-2642-fe0f","💇🏽♂":"1f487-1f3fd-200d-2642-fe0f","💇🏾♂":"1f487-1f3fe-200d-2642-fe0f","💇🏿♂":"1f487-1f3ff-200d-2642-fe0f","💇♀️":"1f487-200d-2640-fe0f","💇🏻♀":"1f487-1f3fb-200d-2640-fe0f","💇🏼♀":"1f487-1f3fc-200d-2640-fe0f","💇🏽♀":"1f487-1f3fd-200d-2640-fe0f","💇🏾♀":"1f487-1f3fe-200d-2640-fe0f","💇🏿♀":"1f487-1f3ff-200d-2640-fe0f","🚶♂️":"1f6b6-200d-2642-fe0f","🚶🏻♂":"1f6b6-1f3fb-200d-2642-fe0f","🚶🏼♂":"1f6b6-1f3fc-200d-2642-fe0f","🚶🏽♂":"1f6b6-1f3fd-200d-2642-fe0f","🚶🏾♂":"1f6b6-1f3fe-200d-2642-fe0f","🚶🏿♂":"1f6b6-1f3ff-200d-2642-fe0f","🚶♀️":"1f6b6-200d-2640-fe0f","🚶🏻♀":"1f6b6-1f3fb-200d-2640-fe0f","🚶🏼♀":"1f6b6-1f3fc-200d-2640-fe0f","🚶🏽♀":"1f6b6-1f3fd-200d-2640-fe0f","🚶🏾♀":"1f6b6-1f3fe-200d-2640-fe0f","🚶🏿♀":"1f6b6-1f3ff-200d-2640-fe0f","🏃♂️":"1f3c3-200d-2642-fe0f","🏃🏻♂":"1f3c3-1f3fb-200d-2642-fe0f","🏃🏼♂":"1f3c3-1f3fc-200d-2642-fe0f","🏃🏽♂":"1f3c3-1f3fd-200d-2642-fe0f","🏃🏾♂":"1f3c3-1f3fe-200d-2642-fe0f","🏃🏿♂":"1f3c3-1f3ff-200d-2642-fe0f","🏃♀️":"1f3c3-200d-2640-fe0f","🏃🏻♀":"1f3c3-1f3fb-200d-2640-fe0f","🏃🏼♀":"1f3c3-1f3fc-200d-2640-fe0f","🏃🏽♀":"1f3c3-1f3fd-200d-2640-fe0f","🏃🏾♀":"1f3c3-1f3fe-200d-2640-fe0f","🏃🏿♀":"1f3c3-1f3ff-200d-2640-fe0f","👯♂️":"1f46f-200d-2642-fe0f","👯♀️":"1f46f-200d-2640-fe0f","🧖♀️":"1f9d6-200d-2640-fe0f","🧖🏻♀":"1f9d6-1f3fb-200d-2640-fe0f","🧖🏼♀":"1f9d6-1f3fc-200d-2640-fe0f","🧖🏽♀":"1f9d6-1f3fd-200d-2640-fe0f","🧖🏾♀":"1f9d6-1f3fe-200d-2640-fe0f","🧖🏿♀":"1f9d6-1f3ff-200d-2640-fe0f","🧖♂️":"1f9d6-200d-2642-fe0f","🧖🏻♂":"1f9d6-1f3fb-200d-2642-fe0f","🧖🏼♂":"1f9d6-1f3fc-200d-2642-fe0f","🧖🏽♂":"1f9d6-1f3fd-200d-2642-fe0f","🧖🏾♂":"1f9d6-1f3fe-200d-2642-fe0f","🧖🏿♂":"1f9d6-1f3ff-200d-2642-fe0f","🧗♀️":"1f9d7-200d-2640-fe0f","🧗🏻♀":"1f9d7-1f3fb-200d-2640-fe0f","🧗🏼♀":"1f9d7-1f3fc-200d-2640-fe0f","🧗🏽♀":"1f9d7-1f3fd-200d-2640-fe0f","🧗🏾♀":"1f9d7-1f3fe-200d-2640-fe0f","🧗🏿♀":"1f9d7-1f3ff-200d-2640-fe0f","🧗♂️":"1f9d7-200d-2642-fe0f","🧗🏻♂":"1f9d7-1f3fb-200d-2642-fe0f","🧗🏼♂":"1f9d7-1f3fc-200d-2642-fe0f","🧗🏽♂":"1f9d7-1f3fd-200d-2642-fe0f","🧗🏾♂":"1f9d7-1f3fe-200d-2642-fe0f","🧗🏿♂":"1f9d7-1f3ff-200d-2642-fe0f","🧘♀️":"1f9d8-200d-2640-fe0f","🧘🏻♀":"1f9d8-1f3fb-200d-2640-fe0f","🧘🏼♀":"1f9d8-1f3fc-200d-2640-fe0f","🧘🏽♀":"1f9d8-1f3fd-200d-2640-fe0f","🧘🏾♀":"1f9d8-1f3fe-200d-2640-fe0f","🧘🏿♀":"1f9d8-1f3ff-200d-2640-fe0f","🧘♂️":"1f9d8-200d-2642-fe0f","🧘🏻♂":"1f9d8-1f3fb-200d-2642-fe0f","🧘🏼♂":"1f9d8-1f3fc-200d-2642-fe0f","🧘🏽♂":"1f9d8-1f3fd-200d-2642-fe0f","🧘🏾♂":"1f9d8-1f3fe-200d-2642-fe0f","🧘🏿♂":"1f9d8-1f3ff-200d-2642-fe0f","🏌♂️":"1f3cc-fe0f-200d-2642-fe0f","🏌️♂":"1f3cc-fe0f-200d-2642-fe0f","🏌🏻♂":"1f3cc-1f3fb-200d-2642-fe0f","🏌🏼♂":"1f3cc-1f3fc-200d-2642-fe0f","🏌🏽♂":"1f3cc-1f3fd-200d-2642-fe0f","🏌🏾♂":"1f3cc-1f3fe-200d-2642-fe0f","🏌🏿♂":"1f3cc-1f3ff-200d-2642-fe0f","🏌♀️":"1f3cc-fe0f-200d-2640-fe0f","🏌️♀":"1f3cc-fe0f-200d-2640-fe0f","🏌🏻♀":"1f3cc-1f3fb-200d-2640-fe0f","🏌🏼♀":"1f3cc-1f3fc-200d-2640-fe0f","🏌🏽♀":"1f3cc-1f3fd-200d-2640-fe0f","🏌🏾♀":"1f3cc-1f3fe-200d-2640-fe0f","🏌🏿♀":"1f3cc-1f3ff-200d-2640-fe0f","🏄♂️":"1f3c4-200d-2642-fe0f","🏄🏻♂":"1f3c4-1f3fb-200d-2642-fe0f","🏄🏼♂":"1f3c4-1f3fc-200d-2642-fe0f","🏄🏽♂":"1f3c4-1f3fd-200d-2642-fe0f","🏄🏾♂":"1f3c4-1f3fe-200d-2642-fe0f","🏄🏿♂":"1f3c4-1f3ff-200d-2642-fe0f","🏄♀️":"1f3c4-200d-2640-fe0f","🏄🏻♀":"1f3c4-1f3fb-200d-2640-fe0f","🏄🏼♀":"1f3c4-1f3fc-200d-2640-fe0f","🏄🏽♀":"1f3c4-1f3fd-200d-2640-fe0f","🏄🏾♀":"1f3c4-1f3fe-200d-2640-fe0f","🏄🏿♀":"1f3c4-1f3ff-200d-2640-fe0f","🚣♂️":"1f6a3-200d-2642-fe0f","🚣🏻♂":"1f6a3-1f3fb-200d-2642-fe0f","🚣🏼♂":"1f6a3-1f3fc-200d-2642-fe0f","🚣🏽♂":"1f6a3-1f3fd-200d-2642-fe0f","🚣🏾♂":"1f6a3-1f3fe-200d-2642-fe0f","🚣🏿♂":"1f6a3-1f3ff-200d-2642-fe0f","🚣♀️":"1f6a3-200d-2640-fe0f","🚣🏻♀":"1f6a3-1f3fb-200d-2640-fe0f","🚣🏼♀":"1f6a3-1f3fc-200d-2640-fe0f","🚣🏽♀":"1f6a3-1f3fd-200d-2640-fe0f","🚣🏾♀":"1f6a3-1f3fe-200d-2640-fe0f","🚣🏿♀":"1f6a3-1f3ff-200d-2640-fe0f","🏊♂️":"1f3ca-200d-2642-fe0f","🏊🏻♂":"1f3ca-1f3fb-200d-2642-fe0f","🏊🏼♂":"1f3ca-1f3fc-200d-2642-fe0f","🏊🏽♂":"1f3ca-1f3fd-200d-2642-fe0f","🏊🏾♂":"1f3ca-1f3fe-200d-2642-fe0f","🏊🏿♂":"1f3ca-1f3ff-200d-2642-fe0f","🏊♀️":"1f3ca-200d-2640-fe0f","🏊🏻♀":"1f3ca-1f3fb-200d-2640-fe0f","🏊🏼♀":"1f3ca-1f3fc-200d-2640-fe0f","🏊🏽♀":"1f3ca-1f3fd-200d-2640-fe0f","🏊🏾♀":"1f3ca-1f3fe-200d-2640-fe0f","🏊🏿♀":"1f3ca-1f3ff-200d-2640-fe0f","⛹♂️":"26f9-fe0f-200d-2642-fe0f","⛹️♂":"26f9-fe0f-200d-2642-fe0f","⛹🏻♂":"26f9-1f3fb-200d-2642-fe0f","⛹🏼♂":"26f9-1f3fc-200d-2642-fe0f","⛹🏽♂":"26f9-1f3fd-200d-2642-fe0f","⛹🏾♂":"26f9-1f3fe-200d-2642-fe0f","⛹🏿♂":"26f9-1f3ff-200d-2642-fe0f","⛹♀️":"26f9-fe0f-200d-2640-fe0f","⛹️♀":"26f9-fe0f-200d-2640-fe0f","⛹🏻♀":"26f9-1f3fb-200d-2640-fe0f","⛹🏼♀":"26f9-1f3fc-200d-2640-fe0f","⛹🏽♀":"26f9-1f3fd-200d-2640-fe0f","⛹🏾♀":"26f9-1f3fe-200d-2640-fe0f","⛹🏿♀":"26f9-1f3ff-200d-2640-fe0f","🏋♂️":"1f3cb-fe0f-200d-2642-fe0f","🏋️♂":"1f3cb-fe0f-200d-2642-fe0f","🏋🏻♂":"1f3cb-1f3fb-200d-2642-fe0f","🏋🏼♂":"1f3cb-1f3fc-200d-2642-fe0f","🏋🏽♂":"1f3cb-1f3fd-200d-2642-fe0f","🏋🏾♂":"1f3cb-1f3fe-200d-2642-fe0f","🏋🏿♂":"1f3cb-1f3ff-200d-2642-fe0f","🏋♀️":"1f3cb-fe0f-200d-2640-fe0f","🏋️♀":"1f3cb-fe0f-200d-2640-fe0f","🏋🏻♀":"1f3cb-1f3fb-200d-2640-fe0f","🏋🏼♀":"1f3cb-1f3fc-200d-2640-fe0f","🏋🏽♀":"1f3cb-1f3fd-200d-2640-fe0f","🏋🏾♀":"1f3cb-1f3fe-200d-2640-fe0f","🏋🏿♀":"1f3cb-1f3ff-200d-2640-fe0f","🚴♂️":"1f6b4-200d-2642-fe0f","🚴🏻♂":"1f6b4-1f3fb-200d-2642-fe0f","🚴🏼♂":"1f6b4-1f3fc-200d-2642-fe0f","🚴🏽♂":"1f6b4-1f3fd-200d-2642-fe0f","🚴🏾♂":"1f6b4-1f3fe-200d-2642-fe0f","🚴🏿♂":"1f6b4-1f3ff-200d-2642-fe0f","🚴♀️":"1f6b4-200d-2640-fe0f","🚴🏻♀":"1f6b4-1f3fb-200d-2640-fe0f","🚴🏼♀":"1f6b4-1f3fc-200d-2640-fe0f","🚴🏽♀":"1f6b4-1f3fd-200d-2640-fe0f","🚴🏾♀":"1f6b4-1f3fe-200d-2640-fe0f","🚴🏿♀":"1f6b4-1f3ff-200d-2640-fe0f","🚵♂️":"1f6b5-200d-2642-fe0f","🚵🏻♂":"1f6b5-1f3fb-200d-2642-fe0f","🚵🏼♂":"1f6b5-1f3fc-200d-2642-fe0f","🚵🏽♂":"1f6b5-1f3fd-200d-2642-fe0f","🚵🏾♂":"1f6b5-1f3fe-200d-2642-fe0f","🚵🏿♂":"1f6b5-1f3ff-200d-2642-fe0f","🚵♀️":"1f6b5-200d-2640-fe0f","🚵🏻♀":"1f6b5-1f3fb-200d-2640-fe0f","🚵🏼♀":"1f6b5-1f3fc-200d-2640-fe0f","🚵🏽♀":"1f6b5-1f3fd-200d-2640-fe0f","🚵🏾♀":"1f6b5-1f3fe-200d-2640-fe0f","🚵🏿♀":"1f6b5-1f3ff-200d-2640-fe0f","🤸♂️":"1f938-200d-2642-fe0f","🤸🏻♂":"1f938-1f3fb-200d-2642-fe0f","🤸🏼♂":"1f938-1f3fc-200d-2642-fe0f","🤸🏽♂":"1f938-1f3fd-200d-2642-fe0f","🤸🏾♂":"1f938-1f3fe-200d-2642-fe0f","🤸🏿♂":"1f938-1f3ff-200d-2642-fe0f","🤸♀️":"1f938-200d-2640-fe0f","🤸🏻♀":"1f938-1f3fb-200d-2640-fe0f","🤸🏼♀":"1f938-1f3fc-200d-2640-fe0f","🤸🏽♀":"1f938-1f3fd-200d-2640-fe0f","🤸🏾♀":"1f938-1f3fe-200d-2640-fe0f","🤸🏿♀":"1f938-1f3ff-200d-2640-fe0f","🤼♂️":"1f93c-200d-2642-fe0f","🤼♀️":"1f93c-200d-2640-fe0f","🤽♂️":"1f93d-200d-2642-fe0f","🤽🏻♂":"1f93d-1f3fb-200d-2642-fe0f","🤽🏼♂":"1f93d-1f3fc-200d-2642-fe0f","🤽🏽♂":"1f93d-1f3fd-200d-2642-fe0f","🤽🏾♂":"1f93d-1f3fe-200d-2642-fe0f","🤽🏿♂":"1f93d-1f3ff-200d-2642-fe0f","🤽♀️":"1f93d-200d-2640-fe0f","🤽🏻♀":"1f93d-1f3fb-200d-2640-fe0f","🤽🏼♀":"1f93d-1f3fc-200d-2640-fe0f","🤽🏽♀":"1f93d-1f3fd-200d-2640-fe0f","🤽🏾♀":"1f93d-1f3fe-200d-2640-fe0f","🤽🏿♀":"1f93d-1f3ff-200d-2640-fe0f","🤾♂️":"1f93e-200d-2642-fe0f","🤾🏻♂":"1f93e-1f3fb-200d-2642-fe0f","🤾🏼♂":"1f93e-1f3fc-200d-2642-fe0f","🤾🏽♂":"1f93e-1f3fd-200d-2642-fe0f","🤾🏾♂":"1f93e-1f3fe-200d-2642-fe0f","🤾🏿♂":"1f93e-1f3ff-200d-2642-fe0f","🤾♀️":"1f93e-200d-2640-fe0f","🤾🏻♀":"1f93e-1f3fb-200d-2640-fe0f","🤾🏼♀":"1f93e-1f3fc-200d-2640-fe0f","🤾🏽♀":"1f93e-1f3fd-200d-2640-fe0f","🤾🏾♀":"1f93e-1f3fe-200d-2640-fe0f","🤾🏿♀":"1f93e-1f3ff-200d-2640-fe0f","🤹♂️":"1f939-200d-2642-fe0f","🤹🏻♂":"1f939-1f3fb-200d-2642-fe0f","🤹🏼♂":"1f939-1f3fc-200d-2642-fe0f","🤹🏽♂":"1f939-1f3fd-200d-2642-fe0f","🤹🏾♂":"1f939-1f3fe-200d-2642-fe0f","🤹🏿♂":"1f939-1f3ff-200d-2642-fe0f","🤹♀️":"1f939-200d-2640-fe0f","🤹🏻♀":"1f939-1f3fb-200d-2640-fe0f","🤹🏼♀":"1f939-1f3fc-200d-2640-fe0f","🤹🏽♀":"1f939-1f3fd-200d-2640-fe0f","🤹🏾♀":"1f939-1f3fe-200d-2640-fe0f","🤹🏿♀":"1f939-1f3ff-200d-2640-fe0f","👁🗨️":"1f441-200d-1f5e8","👁️🗨":"1f441-200d-1f5e8","🏳️🌈":"1f3f3-fe0f-200d-1f308","👨🏻⚕️":"1f468-1f3fb-200d-2695-fe0f","👨🏼⚕️":"1f468-1f3fc-200d-2695-fe0f","👨🏽⚕️":"1f468-1f3fd-200d-2695-fe0f","👨🏾⚕️":"1f468-1f3fe-200d-2695-fe0f","👨🏿⚕️":"1f468-1f3ff-200d-2695-fe0f","👩🏻⚕️":"1f469-1f3fb-200d-2695-fe0f","👩🏼⚕️":"1f469-1f3fc-200d-2695-fe0f","👩🏽⚕️":"1f469-1f3fd-200d-2695-fe0f","👩🏾⚕️":"1f469-1f3fe-200d-2695-fe0f","👩🏿⚕️":"1f469-1f3ff-200d-2695-fe0f","👨🏻⚖️":"1f468-1f3fb-200d-2696-fe0f","👨🏼⚖️":"1f468-1f3fc-200d-2696-fe0f","👨🏽⚖️":"1f468-1f3fd-200d-2696-fe0f","👨🏾⚖️":"1f468-1f3fe-200d-2696-fe0f","👨🏿⚖️":"1f468-1f3ff-200d-2696-fe0f","👩🏻⚖️":"1f469-1f3fb-200d-2696-fe0f","👩🏼⚖️":"1f469-1f3fc-200d-2696-fe0f","👩🏽⚖️":"1f469-1f3fd-200d-2696-fe0f","👩🏾⚖️":"1f469-1f3fe-200d-2696-fe0f","👩🏿⚖️":"1f469-1f3ff-200d-2696-fe0f","👨🏻✈️":"1f468-1f3fb-200d-2708-fe0f","👨🏼✈️":"1f468-1f3fc-200d-2708-fe0f","👨🏽✈️":"1f468-1f3fd-200d-2708-fe0f","👨🏾✈️":"1f468-1f3fe-200d-2708-fe0f","👨🏿✈️":"1f468-1f3ff-200d-2708-fe0f","👩🏻✈️":"1f469-1f3fb-200d-2708-fe0f","👩🏼✈️":"1f469-1f3fc-200d-2708-fe0f","👩🏽✈️":"1f469-1f3fd-200d-2708-fe0f","👩🏾✈️":"1f469-1f3fe-200d-2708-fe0f","👩🏿✈️":"1f469-1f3ff-200d-2708-fe0f","👮🏻♂️":"1f46e-1f3fb-200d-2642-fe0f","👮🏼♂️":"1f46e-1f3fc-200d-2642-fe0f","👮🏽♂️":"1f46e-1f3fd-200d-2642-fe0f","👮🏾♂️":"1f46e-1f3fe-200d-2642-fe0f","👮🏿♂️":"1f46e-1f3ff-200d-2642-fe0f","👮🏻♀️":"1f46e-1f3fb-200d-2640-fe0f","👮🏼♀️":"1f46e-1f3fc-200d-2640-fe0f","👮🏽♀️":"1f46e-1f3fd-200d-2640-fe0f","👮🏾♀️":"1f46e-1f3fe-200d-2640-fe0f","👮🏿♀️":"1f46e-1f3ff-200d-2640-fe0f","🕵️♂️":"1f575-fe0f-200d-2642-fe0f","🕵🏻♂️":"1f575-1f3fb-200d-2642-fe0f","🕵🏼♂️":"1f575-1f3fc-200d-2642-fe0f","🕵🏽♂️":"1f575-1f3fd-200d-2642-fe0f","🕵🏾♂️":"1f575-1f3fe-200d-2642-fe0f","🕵🏿♂️":"1f575-1f3ff-200d-2642-fe0f","🕵️♀️":"1f575-fe0f-200d-2640-fe0f","🕵🏻♀️":"1f575-1f3fb-200d-2640-fe0f","🕵🏼♀️":"1f575-1f3fc-200d-2640-fe0f","🕵🏽♀️":"1f575-1f3fd-200d-2640-fe0f","🕵🏾♀️":"1f575-1f3fe-200d-2640-fe0f","🕵🏿♀️":"1f575-1f3ff-200d-2640-fe0f","💂🏻♂️":"1f482-1f3fb-200d-2642-fe0f","💂🏼♂️":"1f482-1f3fc-200d-2642-fe0f","💂🏽♂️":"1f482-1f3fd-200d-2642-fe0f","💂🏾♂️":"1f482-1f3fe-200d-2642-fe0f","💂🏿♂️":"1f482-1f3ff-200d-2642-fe0f","💂🏻♀️":"1f482-1f3fb-200d-2640-fe0f","💂🏼♀️":"1f482-1f3fc-200d-2640-fe0f","💂🏽♀️":"1f482-1f3fd-200d-2640-fe0f","💂🏾♀️":"1f482-1f3fe-200d-2640-fe0f","💂🏿♀️":"1f482-1f3ff-200d-2640-fe0f","👷🏻♂️":"1f477-1f3fb-200d-2642-fe0f","👷🏼♂️":"1f477-1f3fc-200d-2642-fe0f","👷🏽♂️":"1f477-1f3fd-200d-2642-fe0f","👷🏾♂️":"1f477-1f3fe-200d-2642-fe0f","👷🏿♂️":"1f477-1f3ff-200d-2642-fe0f","👷🏻♀️":"1f477-1f3fb-200d-2640-fe0f","👷🏼♀️":"1f477-1f3fc-200d-2640-fe0f","👷🏽♀️":"1f477-1f3fd-200d-2640-fe0f","👷🏾♀️":"1f477-1f3fe-200d-2640-fe0f","👷🏿♀️":"1f477-1f3ff-200d-2640-fe0f","👳🏻♂️":"1f473-1f3fb-200d-2642-fe0f","👳🏼♂️":"1f473-1f3fc-200d-2642-fe0f","👳🏽♂️":"1f473-1f3fd-200d-2642-fe0f","👳🏾♂️":"1f473-1f3fe-200d-2642-fe0f","👳🏿♂️":"1f473-1f3ff-200d-2642-fe0f","👳🏻♀️":"1f473-1f3fb-200d-2640-fe0f","👳🏼♀️":"1f473-1f3fc-200d-2640-fe0f","👳🏽♀️":"1f473-1f3fd-200d-2640-fe0f","👳🏾♀️":"1f473-1f3fe-200d-2640-fe0f","👳🏿♀️":"1f473-1f3ff-200d-2640-fe0f","👱🏻♂️":"1f471-1f3fb-200d-2642-fe0f","👱🏼♂️":"1f471-1f3fc-200d-2642-fe0f","👱🏽♂️":"1f471-1f3fd-200d-2642-fe0f","👱🏾♂️":"1f471-1f3fe-200d-2642-fe0f","👱🏿♂️":"1f471-1f3ff-200d-2642-fe0f","👱🏻♀️":"1f471-1f3fb-200d-2640-fe0f","👱🏼♀️":"1f471-1f3fc-200d-2640-fe0f","👱🏽♀️":"1f471-1f3fd-200d-2640-fe0f","👱🏾♀️":"1f471-1f3fe-200d-2640-fe0f","👱🏿♀️":"1f471-1f3ff-200d-2640-fe0f","🧙🏻♀️":"1f9d9-1f3fb-200d-2640-fe0f","🧙🏼♀️":"1f9d9-1f3fc-200d-2640-fe0f","🧙🏽♀️":"1f9d9-1f3fd-200d-2640-fe0f","🧙🏾♀️":"1f9d9-1f3fe-200d-2640-fe0f","🧙🏿♀️":"1f9d9-1f3ff-200d-2640-fe0f","🧙🏻♂️":"1f9d9-1f3fb-200d-2642-fe0f","🧙🏼♂️":"1f9d9-1f3fc-200d-2642-fe0f","🧙🏽♂️":"1f9d9-1f3fd-200d-2642-fe0f","🧙🏾♂️":"1f9d9-1f3fe-200d-2642-fe0f","🧙🏿♂️":"1f9d9-1f3ff-200d-2642-fe0f","🧚🏻♀️":"1f9da-1f3fb-200d-2640-fe0f","🧚🏼♀️":"1f9da-1f3fc-200d-2640-fe0f","🧚🏽♀️":"1f9da-1f3fd-200d-2640-fe0f","🧚🏾♀️":"1f9da-1f3fe-200d-2640-fe0f","🧚🏿♀️":"1f9da-1f3ff-200d-2640-fe0f","🧚🏻♂️":"1f9da-1f3fb-200d-2642-fe0f","🧚🏼♂️":"1f9da-1f3fc-200d-2642-fe0f","🧚🏽♂️":"1f9da-1f3fd-200d-2642-fe0f","🧚🏾♂️":"1f9da-1f3fe-200d-2642-fe0f","🧚🏿♂️":"1f9da-1f3ff-200d-2642-fe0f","🧛🏻♀️":"1f9db-1f3fb-200d-2640-fe0f","🧛🏼♀️":"1f9db-1f3fc-200d-2640-fe0f","🧛🏽♀️":"1f9db-1f3fd-200d-2640-fe0f","🧛🏾♀️":"1f9db-1f3fe-200d-2640-fe0f","🧛🏿♀️":"1f9db-1f3ff-200d-2640-fe0f","🧛🏻♂️":"1f9db-1f3fb-200d-2642-fe0f","🧛🏼♂️":"1f9db-1f3fc-200d-2642-fe0f","🧛🏽♂️":"1f9db-1f3fd-200d-2642-fe0f","🧛🏾♂️":"1f9db-1f3fe-200d-2642-fe0f","🧛🏿♂️":"1f9db-1f3ff-200d-2642-fe0f","🧜🏻♀️":"1f9dc-1f3fb-200d-2640-fe0f","🧜🏼♀️":"1f9dc-1f3fc-200d-2640-fe0f","🧜🏽♀️":"1f9dc-1f3fd-200d-2640-fe0f","🧜🏾♀️":"1f9dc-1f3fe-200d-2640-fe0f","🧜🏿♀️":"1f9dc-1f3ff-200d-2640-fe0f","🧜🏻♂️":"1f9dc-1f3fb-200d-2642-fe0f","🧜🏼♂️":"1f9dc-1f3fc-200d-2642-fe0f","🧜🏽♂️":"1f9dc-1f3fd-200d-2642-fe0f","🧜🏾♂️":"1f9dc-1f3fe-200d-2642-fe0f","🧜🏿♂️":"1f9dc-1f3ff-200d-2642-fe0f","🧝🏻♀️":"1f9dd-1f3fb-200d-2640-fe0f","🧝🏼♀️":"1f9dd-1f3fc-200d-2640-fe0f","🧝🏽♀️":"1f9dd-1f3fd-200d-2640-fe0f","🧝🏾♀️":"1f9dd-1f3fe-200d-2640-fe0f","🧝🏿♀️":"1f9dd-1f3ff-200d-2640-fe0f","🧝🏻♂️":"1f9dd-1f3fb-200d-2642-fe0f","🧝🏼♂️":"1f9dd-1f3fc-200d-2642-fe0f","🧝🏽♂️":"1f9dd-1f3fd-200d-2642-fe0f","🧝🏾♂️":"1f9dd-1f3fe-200d-2642-fe0f","🧝🏿♂️":"1f9dd-1f3ff-200d-2642-fe0f","🙍🏻♂️":"1f64d-1f3fb-200d-2642-fe0f","🙍🏼♂️":"1f64d-1f3fc-200d-2642-fe0f","🙍🏽♂️":"1f64d-1f3fd-200d-2642-fe0f","🙍🏾♂️":"1f64d-1f3fe-200d-2642-fe0f","🙍🏿♂️":"1f64d-1f3ff-200d-2642-fe0f","🙍🏻♀️":"1f64d-1f3fb-200d-2640-fe0f","🙍🏼♀️":"1f64d-1f3fc-200d-2640-fe0f","🙍🏽♀️":"1f64d-1f3fd-200d-2640-fe0f","🙍🏾♀️":"1f64d-1f3fe-200d-2640-fe0f","🙍🏿♀️":"1f64d-1f3ff-200d-2640-fe0f","🙎🏻♂️":"1f64e-1f3fb-200d-2642-fe0f","🙎🏼♂️":"1f64e-1f3fc-200d-2642-fe0f","🙎🏽♂️":"1f64e-1f3fd-200d-2642-fe0f","🙎🏾♂️":"1f64e-1f3fe-200d-2642-fe0f","🙎🏿♂️":"1f64e-1f3ff-200d-2642-fe0f","🙎🏻♀️":"1f64e-1f3fb-200d-2640-fe0f","🙎🏼♀️":"1f64e-1f3fc-200d-2640-fe0f","🙎🏽♀️":"1f64e-1f3fd-200d-2640-fe0f","🙎🏾♀️":"1f64e-1f3fe-200d-2640-fe0f","🙎🏿♀️":"1f64e-1f3ff-200d-2640-fe0f","🙅🏻♂️":"1f645-1f3fb-200d-2642-fe0f","🙅🏼♂️":"1f645-1f3fc-200d-2642-fe0f","🙅🏽♂️":"1f645-1f3fd-200d-2642-fe0f","🙅🏾♂️":"1f645-1f3fe-200d-2642-fe0f","🙅🏿♂️":"1f645-1f3ff-200d-2642-fe0f","🙅🏻♀️":"1f645-1f3fb-200d-2640-fe0f","🙅🏼♀️":"1f645-1f3fc-200d-2640-fe0f","🙅🏽♀️":"1f645-1f3fd-200d-2640-fe0f","🙅🏾♀️":"1f645-1f3fe-200d-2640-fe0f","🙅🏿♀️":"1f645-1f3ff-200d-2640-fe0f","🙆🏻♂️":"1f646-1f3fb-200d-2642-fe0f","🙆🏼♂️":"1f646-1f3fc-200d-2642-fe0f","🙆🏽♂️":"1f646-1f3fd-200d-2642-fe0f","🙆🏾♂️":"1f646-1f3fe-200d-2642-fe0f","🙆🏿♂️":"1f646-1f3ff-200d-2642-fe0f","🙆🏻♀️":"1f646-1f3fb-200d-2640-fe0f","🙆🏼♀️":"1f646-1f3fc-200d-2640-fe0f","🙆🏽♀️":"1f646-1f3fd-200d-2640-fe0f","🙆🏾♀️":"1f646-1f3fe-200d-2640-fe0f","🙆🏿♀️":"1f646-1f3ff-200d-2640-fe0f","💁🏻♂️":"1f481-1f3fb-200d-2642-fe0f","💁🏼♂️":"1f481-1f3fc-200d-2642-fe0f","💁🏽♂️":"1f481-1f3fd-200d-2642-fe0f","💁🏾♂️":"1f481-1f3fe-200d-2642-fe0f","💁🏿♂️":"1f481-1f3ff-200d-2642-fe0f","💁🏻♀️":"1f481-1f3fb-200d-2640-fe0f","💁🏼♀️":"1f481-1f3fc-200d-2640-fe0f","💁🏽♀️":"1f481-1f3fd-200d-2640-fe0f","💁🏾♀️":"1f481-1f3fe-200d-2640-fe0f","💁🏿♀️":"1f481-1f3ff-200d-2640-fe0f","🙋🏻♂️":"1f64b-1f3fb-200d-2642-fe0f","🙋🏼♂️":"1f64b-1f3fc-200d-2642-fe0f","🙋🏽♂️":"1f64b-1f3fd-200d-2642-fe0f","🙋🏾♂️":"1f64b-1f3fe-200d-2642-fe0f","🙋🏿♂️":"1f64b-1f3ff-200d-2642-fe0f","🙋🏻♀️":"1f64b-1f3fb-200d-2640-fe0f","🙋🏼♀️":"1f64b-1f3fc-200d-2640-fe0f","🙋🏽♀️":"1f64b-1f3fd-200d-2640-fe0f","🙋🏾♀️":"1f64b-1f3fe-200d-2640-fe0f","🙋🏿♀️":"1f64b-1f3ff-200d-2640-fe0f","🙇🏻♂️":"1f647-1f3fb-200d-2642-fe0f","🙇🏼♂️":"1f647-1f3fc-200d-2642-fe0f","🙇🏽♂️":"1f647-1f3fd-200d-2642-fe0f","🙇🏾♂️":"1f647-1f3fe-200d-2642-fe0f","🙇🏿♂️":"1f647-1f3ff-200d-2642-fe0f","🙇🏻♀️":"1f647-1f3fb-200d-2640-fe0f","🙇🏼♀️":"1f647-1f3fc-200d-2640-fe0f","🙇🏽♀️":"1f647-1f3fd-200d-2640-fe0f","🙇🏾♀️":"1f647-1f3fe-200d-2640-fe0f","🙇🏿♀️":"1f647-1f3ff-200d-2640-fe0f","🤦🏻♂️":"1f926-1f3fb-200d-2642-fe0f","🤦🏼♂️":"1f926-1f3fc-200d-2642-fe0f","🤦🏽♂️":"1f926-1f3fd-200d-2642-fe0f","🤦🏾♂️":"1f926-1f3fe-200d-2642-fe0f","🤦🏿♂️":"1f926-1f3ff-200d-2642-fe0f","🤦🏻♀️":"1f926-1f3fb-200d-2640-fe0f","🤦🏼♀️":"1f926-1f3fc-200d-2640-fe0f","🤦🏽♀️":"1f926-1f3fd-200d-2640-fe0f","🤦🏾♀️":"1f926-1f3fe-200d-2640-fe0f","🤦🏿♀️":"1f926-1f3ff-200d-2640-fe0f","🤷🏻♂️":"1f937-1f3fb-200d-2642-fe0f","🤷🏼♂️":"1f937-1f3fc-200d-2642-fe0f","🤷🏽♂️":"1f937-1f3fd-200d-2642-fe0f","🤷🏾♂️":"1f937-1f3fe-200d-2642-fe0f","🤷🏿♂️":"1f937-1f3ff-200d-2642-fe0f","🤷🏻♀️":"1f937-1f3fb-200d-2640-fe0f","🤷🏼♀️":"1f937-1f3fc-200d-2640-fe0f","🤷🏽♀️":"1f937-1f3fd-200d-2640-fe0f","🤷🏾♀️":"1f937-1f3fe-200d-2640-fe0f","🤷🏿♀️":"1f937-1f3ff-200d-2640-fe0f","💆🏻♂️":"1f486-1f3fb-200d-2642-fe0f","💆🏼♂️":"1f486-1f3fc-200d-2642-fe0f","💆🏽♂️":"1f486-1f3fd-200d-2642-fe0f","💆🏾♂️":"1f486-1f3fe-200d-2642-fe0f","💆🏿♂️":"1f486-1f3ff-200d-2642-fe0f","💆🏻♀️":"1f486-1f3fb-200d-2640-fe0f","💆🏼♀️":"1f486-1f3fc-200d-2640-fe0f","💆🏽♀️":"1f486-1f3fd-200d-2640-fe0f","💆🏾♀️":"1f486-1f3fe-200d-2640-fe0f","💆🏿♀️":"1f486-1f3ff-200d-2640-fe0f","💇🏻♂️":"1f487-1f3fb-200d-2642-fe0f","💇🏼♂️":"1f487-1f3fc-200d-2642-fe0f","💇🏽♂️":"1f487-1f3fd-200d-2642-fe0f","💇🏾♂️":"1f487-1f3fe-200d-2642-fe0f","💇🏿♂️":"1f487-1f3ff-200d-2642-fe0f","💇🏻♀️":"1f487-1f3fb-200d-2640-fe0f","💇🏼♀️":"1f487-1f3fc-200d-2640-fe0f","💇🏽♀️":"1f487-1f3fd-200d-2640-fe0f","💇🏾♀️":"1f487-1f3fe-200d-2640-fe0f","💇🏿♀️":"1f487-1f3ff-200d-2640-fe0f","🚶🏻♂️":"1f6b6-1f3fb-200d-2642-fe0f","🚶🏼♂️":"1f6b6-1f3fc-200d-2642-fe0f","🚶🏽♂️":"1f6b6-1f3fd-200d-2642-fe0f","🚶🏾♂️":"1f6b6-1f3fe-200d-2642-fe0f","🚶🏿♂️":"1f6b6-1f3ff-200d-2642-fe0f","🚶🏻♀️":"1f6b6-1f3fb-200d-2640-fe0f","🚶🏼♀️":"1f6b6-1f3fc-200d-2640-fe0f","🚶🏽♀️":"1f6b6-1f3fd-200d-2640-fe0f","🚶🏾♀️":"1f6b6-1f3fe-200d-2640-fe0f","🚶🏿♀️":"1f6b6-1f3ff-200d-2640-fe0f","🏃🏻♂️":"1f3c3-1f3fb-200d-2642-fe0f","🏃🏼♂️":"1f3c3-1f3fc-200d-2642-fe0f","🏃🏽♂️":"1f3c3-1f3fd-200d-2642-fe0f","🏃🏾♂️":"1f3c3-1f3fe-200d-2642-fe0f","🏃🏿♂️":"1f3c3-1f3ff-200d-2642-fe0f","🏃🏻♀️":"1f3c3-1f3fb-200d-2640-fe0f","🏃🏼♀️":"1f3c3-1f3fc-200d-2640-fe0f","🏃🏽♀️":"1f3c3-1f3fd-200d-2640-fe0f","🏃🏾♀️":"1f3c3-1f3fe-200d-2640-fe0f","🏃🏿♀️":"1f3c3-1f3ff-200d-2640-fe0f","🧖🏻♀️":"1f9d6-1f3fb-200d-2640-fe0f","🧖🏼♀️":"1f9d6-1f3fc-200d-2640-fe0f","🧖🏽♀️":"1f9d6-1f3fd-200d-2640-fe0f","🧖🏾♀️":"1f9d6-1f3fe-200d-2640-fe0f","🧖🏿♀️":"1f9d6-1f3ff-200d-2640-fe0f","🧖🏻♂️":"1f9d6-1f3fb-200d-2642-fe0f","🧖🏼♂️":"1f9d6-1f3fc-200d-2642-fe0f","🧖🏽♂️":"1f9d6-1f3fd-200d-2642-fe0f","🧖🏾♂️":"1f9d6-1f3fe-200d-2642-fe0f","🧖🏿♂️":"1f9d6-1f3ff-200d-2642-fe0f","🧗🏻♀️":"1f9d7-1f3fb-200d-2640-fe0f","🧗🏼♀️":"1f9d7-1f3fc-200d-2640-fe0f","🧗🏽♀️":"1f9d7-1f3fd-200d-2640-fe0f","🧗🏾♀️":"1f9d7-1f3fe-200d-2640-fe0f","🧗🏿♀️":"1f9d7-1f3ff-200d-2640-fe0f","🧗🏻♂️":"1f9d7-1f3fb-200d-2642-fe0f","🧗🏼♂️":"1f9d7-1f3fc-200d-2642-fe0f","🧗🏽♂️":"1f9d7-1f3fd-200d-2642-fe0f","🧗🏾♂️":"1f9d7-1f3fe-200d-2642-fe0f","🧗🏿♂️":"1f9d7-1f3ff-200d-2642-fe0f","🧘🏻♀️":"1f9d8-1f3fb-200d-2640-fe0f","🧘🏼♀️":"1f9d8-1f3fc-200d-2640-fe0f","🧘🏽♀️":"1f9d8-1f3fd-200d-2640-fe0f","🧘🏾♀️":"1f9d8-1f3fe-200d-2640-fe0f","🧘🏿♀️":"1f9d8-1f3ff-200d-2640-fe0f","🧘🏻♂️":"1f9d8-1f3fb-200d-2642-fe0f","🧘🏼♂️":"1f9d8-1f3fc-200d-2642-fe0f","🧘🏽♂️":"1f9d8-1f3fd-200d-2642-fe0f","🧘🏾♂️":"1f9d8-1f3fe-200d-2642-fe0f","🧘🏿♂️":"1f9d8-1f3ff-200d-2642-fe0f","🏌️♂️":"1f3cc-fe0f-200d-2642-fe0f","🏌🏻♂️":"1f3cc-1f3fb-200d-2642-fe0f","🏌🏼♂️":"1f3cc-1f3fc-200d-2642-fe0f","🏌🏽♂️":"1f3cc-1f3fd-200d-2642-fe0f","🏌🏾♂️":"1f3cc-1f3fe-200d-2642-fe0f","🏌🏿♂️":"1f3cc-1f3ff-200d-2642-fe0f","🏌️♀️":"1f3cc-fe0f-200d-2640-fe0f","🏌🏻♀️":"1f3cc-1f3fb-200d-2640-fe0f","🏌🏼♀️":"1f3cc-1f3fc-200d-2640-fe0f","🏌🏽♀️":"1f3cc-1f3fd-200d-2640-fe0f","🏌🏾♀️":"1f3cc-1f3fe-200d-2640-fe0f","🏌🏿♀️":"1f3cc-1f3ff-200d-2640-fe0f","🏄🏻♂️":"1f3c4-1f3fb-200d-2642-fe0f","🏄🏼♂️":"1f3c4-1f3fc-200d-2642-fe0f","🏄🏽♂️":"1f3c4-1f3fd-200d-2642-fe0f","🏄🏾♂️":"1f3c4-1f3fe-200d-2642-fe0f","🏄🏿♂️":"1f3c4-1f3ff-200d-2642-fe0f","🏄🏻♀️":"1f3c4-1f3fb-200d-2640-fe0f","🏄🏼♀️":"1f3c4-1f3fc-200d-2640-fe0f","🏄🏽♀️":"1f3c4-1f3fd-200d-2640-fe0f","🏄🏾♀️":"1f3c4-1f3fe-200d-2640-fe0f","🏄🏿♀️":"1f3c4-1f3ff-200d-2640-fe0f","🚣🏻♂️":"1f6a3-1f3fb-200d-2642-fe0f","🚣🏼♂️":"1f6a3-1f3fc-200d-2642-fe0f","🚣🏽♂️":"1f6a3-1f3fd-200d-2642-fe0f","🚣🏾♂️":"1f6a3-1f3fe-200d-2642-fe0f","🚣🏿♂️":"1f6a3-1f3ff-200d-2642-fe0f","🚣🏻♀️":"1f6a3-1f3fb-200d-2640-fe0f","🚣🏼♀️":"1f6a3-1f3fc-200d-2640-fe0f","🚣🏽♀️":"1f6a3-1f3fd-200d-2640-fe0f","🚣🏾♀️":"1f6a3-1f3fe-200d-2640-fe0f","🚣🏿♀️":"1f6a3-1f3ff-200d-2640-fe0f","🏊🏻♂️":"1f3ca-1f3fb-200d-2642-fe0f","🏊🏼♂️":"1f3ca-1f3fc-200d-2642-fe0f","🏊🏽♂️":"1f3ca-1f3fd-200d-2642-fe0f","🏊🏾♂️":"1f3ca-1f3fe-200d-2642-fe0f","🏊🏿♂️":"1f3ca-1f3ff-200d-2642-fe0f","🏊🏻♀️":"1f3ca-1f3fb-200d-2640-fe0f","🏊🏼♀️":"1f3ca-1f3fc-200d-2640-fe0f","🏊🏽♀️":"1f3ca-1f3fd-200d-2640-fe0f","🏊🏾♀️":"1f3ca-1f3fe-200d-2640-fe0f","🏊🏿♀️":"1f3ca-1f3ff-200d-2640-fe0f","⛹️♂️":"26f9-fe0f-200d-2642-fe0f","⛹🏻♂️":"26f9-1f3fb-200d-2642-fe0f","⛹🏼♂️":"26f9-1f3fc-200d-2642-fe0f","⛹🏽♂️":"26f9-1f3fd-200d-2642-fe0f","⛹🏾♂️":"26f9-1f3fe-200d-2642-fe0f","⛹🏿♂️":"26f9-1f3ff-200d-2642-fe0f","⛹️♀️":"26f9-fe0f-200d-2640-fe0f","⛹🏻♀️":"26f9-1f3fb-200d-2640-fe0f","⛹🏼♀️":"26f9-1f3fc-200d-2640-fe0f","⛹🏽♀️":"26f9-1f3fd-200d-2640-fe0f","⛹🏾♀️":"26f9-1f3fe-200d-2640-fe0f","⛹🏿♀️":"26f9-1f3ff-200d-2640-fe0f","🏋️♂️":"1f3cb-fe0f-200d-2642-fe0f","🏋🏻♂️":"1f3cb-1f3fb-200d-2642-fe0f","🏋🏼♂️":"1f3cb-1f3fc-200d-2642-fe0f","🏋🏽♂️":"1f3cb-1f3fd-200d-2642-fe0f","🏋🏾♂️":"1f3cb-1f3fe-200d-2642-fe0f","🏋🏿♂️":"1f3cb-1f3ff-200d-2642-fe0f","🏋️♀️":"1f3cb-fe0f-200d-2640-fe0f","🏋🏻♀️":"1f3cb-1f3fb-200d-2640-fe0f","🏋🏼♀️":"1f3cb-1f3fc-200d-2640-fe0f","🏋🏽♀️":"1f3cb-1f3fd-200d-2640-fe0f","🏋🏾♀️":"1f3cb-1f3fe-200d-2640-fe0f","🏋🏿♀️":"1f3cb-1f3ff-200d-2640-fe0f","🚴🏻♂️":"1f6b4-1f3fb-200d-2642-fe0f","🚴🏼♂️":"1f6b4-1f3fc-200d-2642-fe0f","🚴🏽♂️":"1f6b4-1f3fd-200d-2642-fe0f","🚴🏾♂️":"1f6b4-1f3fe-200d-2642-fe0f","🚴🏿♂️":"1f6b4-1f3ff-200d-2642-fe0f","🚴🏻♀️":"1f6b4-1f3fb-200d-2640-fe0f","🚴🏼♀️":"1f6b4-1f3fc-200d-2640-fe0f","🚴🏽♀️":"1f6b4-1f3fd-200d-2640-fe0f","🚴🏾♀️":"1f6b4-1f3fe-200d-2640-fe0f","🚴🏿♀️":"1f6b4-1f3ff-200d-2640-fe0f","🚵🏻♂️":"1f6b5-1f3fb-200d-2642-fe0f","🚵🏼♂️":"1f6b5-1f3fc-200d-2642-fe0f","🚵🏽♂️":"1f6b5-1f3fd-200d-2642-fe0f","🚵🏾♂️":"1f6b5-1f3fe-200d-2642-fe0f","🚵🏿♂️":"1f6b5-1f3ff-200d-2642-fe0f","🚵🏻♀️":"1f6b5-1f3fb-200d-2640-fe0f","🚵🏼♀️":"1f6b5-1f3fc-200d-2640-fe0f","🚵🏽♀️":"1f6b5-1f3fd-200d-2640-fe0f","🚵🏾♀️":"1f6b5-1f3fe-200d-2640-fe0f","🚵🏿♀️":"1f6b5-1f3ff-200d-2640-fe0f","🤸🏻♂️":"1f938-1f3fb-200d-2642-fe0f","🤸🏼♂️":"1f938-1f3fc-200d-2642-fe0f","🤸🏽♂️":"1f938-1f3fd-200d-2642-fe0f","🤸🏾♂️":"1f938-1f3fe-200d-2642-fe0f","🤸🏿♂️":"1f938-1f3ff-200d-2642-fe0f","🤸🏻♀️":"1f938-1f3fb-200d-2640-fe0f","🤸🏼♀️":"1f938-1f3fc-200d-2640-fe0f","🤸🏽♀️":"1f938-1f3fd-200d-2640-fe0f","🤸🏾♀️":"1f938-1f3fe-200d-2640-fe0f","🤸🏿♀️":"1f938-1f3ff-200d-2640-fe0f","🤽🏻♂️":"1f93d-1f3fb-200d-2642-fe0f","🤽🏼♂️":"1f93d-1f3fc-200d-2642-fe0f","🤽🏽♂️":"1f93d-1f3fd-200d-2642-fe0f","🤽🏾♂️":"1f93d-1f3fe-200d-2642-fe0f","🤽🏿♂️":"1f93d-1f3ff-200d-2642-fe0f","🤽🏻♀️":"1f93d-1f3fb-200d-2640-fe0f","🤽🏼♀️":"1f93d-1f3fc-200d-2640-fe0f","🤽🏽♀️":"1f93d-1f3fd-200d-2640-fe0f","🤽🏾♀️":"1f93d-1f3fe-200d-2640-fe0f","🤽🏿♀️":"1f93d-1f3ff-200d-2640-fe0f","🤾🏻♂️":"1f93e-1f3fb-200d-2642-fe0f","🤾🏼♂️":"1f93e-1f3fc-200d-2642-fe0f","🤾🏽♂️":"1f93e-1f3fd-200d-2642-fe0f","🤾🏾♂️":"1f93e-1f3fe-200d-2642-fe0f","🤾🏿♂️":"1f93e-1f3ff-200d-2642-fe0f","🤾🏻♀️":"1f93e-1f3fb-200d-2640-fe0f","🤾🏼♀️":"1f93e-1f3fc-200d-2640-fe0f","🤾🏽♀️":"1f93e-1f3fd-200d-2640-fe0f","🤾🏾♀️":"1f93e-1f3fe-200d-2640-fe0f","🤾🏿♀️":"1f93e-1f3ff-200d-2640-fe0f","🤹🏻♂️":"1f939-1f3fb-200d-2642-fe0f","🤹🏼♂️":"1f939-1f3fc-200d-2642-fe0f","🤹🏽♂️":"1f939-1f3fd-200d-2642-fe0f","🤹🏾♂️":"1f939-1f3fe-200d-2642-fe0f","🤹🏿♂️":"1f939-1f3ff-200d-2642-fe0f","🤹🏻♀️":"1f939-1f3fb-200d-2640-fe0f","🤹🏼♀️":"1f939-1f3fc-200d-2640-fe0f","🤹🏽♀️":"1f939-1f3fd-200d-2640-fe0f","🤹🏾♀️":"1f939-1f3fe-200d-2640-fe0f","🤹🏿♀️":"1f939-1f3ff-200d-2640-fe0f","👩❤👨":"1f469-200d-2764-fe0f-200d-1f468","👨❤👨":"1f468-200d-2764-fe0f-200d-1f468","👩❤👩":"1f469-200d-2764-fe0f-200d-1f469","👨👩👦":"1f468-200d-1f469-200d-1f466","👨👩👧":"1f468-200d-1f469-200d-1f467","👨👨👦":"1f468-200d-1f468-200d-1f466","👨👨👧":"1f468-200d-1f468-200d-1f467","👩👩👦":"1f469-200d-1f469-200d-1f466","👩👩👧":"1f469-200d-1f469-200d-1f467","👨👦👦":"1f468-200d-1f466-200d-1f466","👨👧👦":"1f468-200d-1f467-200d-1f466","👨👧👧":"1f468-200d-1f467-200d-1f467","👩👦👦":"1f469-200d-1f466-200d-1f466","👩👧👦":"1f469-200d-1f467-200d-1f466","👩👧👧":"1f469-200d-1f467-200d-1f467","👁️🗨️":"1f441-200d-1f5e8","👩❤️👨":"1f469-200d-2764-fe0f-200d-1f468","👨❤️👨":"1f468-200d-2764-fe0f-200d-1f468","👩❤️👩":"1f469-200d-2764-fe0f-200d-1f469","👩❤💋👨":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f468","👨❤💋👨":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468","👩❤💋👩":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469","👨👩👧👦":"1f468-200d-1f469-200d-1f467-200d-1f466","👨👩👦👦":"1f468-200d-1f469-200d-1f466-200d-1f466","👨👩👧👧":"1f468-200d-1f469-200d-1f467-200d-1f467","👨👨👧👦":"1f468-200d-1f468-200d-1f467-200d-1f466","👨👨👦👦":"1f468-200d-1f468-200d-1f466-200d-1f466","👨👨👧👧":"1f468-200d-1f468-200d-1f467-200d-1f467","👩👩👧👦":"1f469-200d-1f469-200d-1f467-200d-1f466","👩👩👦👦":"1f469-200d-1f469-200d-1f466-200d-1f466","👩👩👧👧":"1f469-200d-1f469-200d-1f467-200d-1f467","🏴":"1f3f4-e0067-e0062-e0065-e006e-e0067-e007f","🏴":"1f3f4-e0067-e0062-e0073-e0063-e0074-e007f","🏴":"1f3f4-e0067-e0062-e0077-e006c-e0073-e007f","👩❤️💋👨":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f468","👨❤️💋👨":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468","👩❤️💋👩":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469"} \ No newline at end of file diff --git a/app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js b/app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js deleted file mode 100644 index 45086fc4c..000000000 --- a/app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js +++ /dev/null @@ -1,41 +0,0 @@ -// The output of this module is designed to mimic emoji-mart's -// "data" object, such that we can use it for a light version of emoji-mart's -// emojiIndex.search functionality. -const { unicodeToUnifiedName } = require('./unicode_to_unified_name'); -const [ shortCodesToEmojiData, skins, categories, short_names ] = require('./emoji_compressed'); - -const emojis = {}; - -// decompress -Object.keys(shortCodesToEmojiData).forEach((shortCode) => { - let [ - filenameData, // eslint-disable-line no-unused-vars - searchData, - ] = shortCodesToEmojiData[shortCode]; - let [ - native, - short_names, - search, - unified, - ] = searchData; - - if (!unified) { - // unified name can be derived from unicodeToUnifiedName - unified = unicodeToUnifiedName(native); - } - - short_names = [shortCode].concat(short_names); - emojis[shortCode] = { - native, - search, - short_names, - unified, - }; -}); - -module.exports = { - emojis, - skins, - categories, - short_names, -}; diff --git a/app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js b/app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js deleted file mode 100644 index 5755bf1c4..000000000 --- a/app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js +++ /dev/null @@ -1,157 +0,0 @@ -// This code is largely borrowed from: -// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js - -import data from './emoji_mart_data_light'; -import { getData, getSanitizedData, intersect } from './emoji_utils'; - -let originalPool = {}; -let index = {}; -let emojisList = {}; -let emoticonsList = {}; - -for (let emoji in data.emojis) { - let emojiData = data.emojis[emoji]; - let { short_names, emoticons } = emojiData; - let id = short_names[0]; - - if (emoticons) { - emoticons.forEach(emoticon => { - if (emoticonsList[emoticon]) { - return; - } - - emoticonsList[emoticon] = id; - }); - } - - emojisList[id] = getSanitizedData(id); - originalPool[id] = emojiData; -} - -function addCustomToPool(custom, pool) { - custom.forEach((emoji) => { - let emojiId = emoji.id || emoji.short_names[0]; - - if (emojiId && !pool[emojiId]) { - pool[emojiId] = getData(emoji); - emojisList[emojiId] = getSanitizedData(emoji); - } - }); -} - -function search(value, { emojisToShowFilter, maxResults, include, exclude, custom = [] } = {}) { - addCustomToPool(custom, originalPool); - - maxResults = maxResults || 75; - include = include || []; - exclude = exclude || []; - - let results = null, - pool = originalPool; - - if (value.length) { - if (value === '-' || value === '-1') { - return [emojisList['-1']]; - } - - let values = value.toLowerCase().split(/[\s|,|\-|_]+/), - allResults = []; - - if (values.length > 2) { - values = [values[0], values[1]]; - } - - if (include.length || exclude.length) { - pool = {}; - - data.categories.forEach(category => { - let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true; - let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false; - if (!isIncluded || isExcluded) { - return; - } - - category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]); - }); - - if (custom.length) { - let customIsIncluded = include && include.length ? include.indexOf('custom') > -1 : true; - let customIsExcluded = exclude && exclude.length ? exclude.indexOf('custom') > -1 : false; - if (customIsIncluded && !customIsExcluded) { - addCustomToPool(custom, pool); - } - } - } - - allResults = values.map((value) => { - let aPool = pool, - aIndex = index, - length = 0; - - for (let charIndex = 0; charIndex < value.length; charIndex++) { - const char = value[charIndex]; - length++; - - aIndex[char] = aIndex[char] || {}; - aIndex = aIndex[char]; - - if (!aIndex.results) { - let scores = {}; - - aIndex.results = []; - aIndex.pool = {}; - - for (let id in aPool) { - let emoji = aPool[id], - { search } = emoji, - sub = value.substr(0, length), - subIndex = search.indexOf(sub); - - if (subIndex !== -1) { - let score = subIndex + 1; - if (sub === id) score = 0; - - aIndex.results.push(emojisList[id]); - aIndex.pool[id] = emoji; - - scores[id] = score; - } - } - - aIndex.results.sort((a, b) => { - let aScore = scores[a.id], - bScore = scores[b.id]; - - return aScore - bScore; - }); - } - - aPool = aIndex.pool; - } - - return aIndex.results; - }).filter(a => a); - - if (allResults.length > 1) { - results = intersect.apply(null, allResults); - } else if (allResults.length) { - results = allResults[0]; - } else { - results = []; - } - } - - if (results) { - if (emojisToShowFilter) { - results = results.filter((result) => emojisToShowFilter(data.emojis[result.id].unified)); - } - - if (results && results.length > maxResults) { - results = results.slice(0, maxResults); - } - } - - return results; -} - -export { search }; diff --git a/app/javascript/themes/glitch/util/emoji/emoji_picker.js b/app/javascript/themes/glitch/util/emoji/emoji_picker.js deleted file mode 100644 index 7e145381e..000000000 --- a/app/javascript/themes/glitch/util/emoji/emoji_picker.js +++ /dev/null @@ -1,7 +0,0 @@ -import Picker from 'emoji-mart/dist-es/components/picker'; -import Emoji from 'emoji-mart/dist-es/components/emoji'; - -export { - Picker, - Emoji, -}; diff --git a/app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js b/app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js deleted file mode 100644 index 918684c31..000000000 --- a/app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js +++ /dev/null @@ -1,35 +0,0 @@ -// A mapping of unicode strings to an object containing the filename -// (i.e. the svg filename) and a shortCode intended to be shown -// as a "title" attribute in an HTML element (aka tooltip). - -const [ - shortCodesToEmojiData, - skins, // eslint-disable-line no-unused-vars - categories, // eslint-disable-line no-unused-vars - short_names, // eslint-disable-line no-unused-vars - emojisWithoutShortCodes, -] = require('./emoji_compressed'); -const { unicodeToFilename } = require('./unicode_to_filename'); - -// decompress -const unicodeMapping = {}; - -function processEmojiMapData(emojiMapData, shortCode) { - let [ native, filename ] = emojiMapData; - if (!filename) { - // filename name can be derived from unicodeToFilename - filename = unicodeToFilename(native); - } - unicodeMapping[native] = { - shortCode: shortCode, - filename: filename, - }; -} - -Object.keys(shortCodesToEmojiData).forEach((shortCode) => { - let [ filenameData ] = shortCodesToEmojiData[shortCode]; - filenameData.forEach(emojiMapData => processEmojiMapData(emojiMapData, shortCode)); -}); -emojisWithoutShortCodes.forEach(emojiMapData => processEmojiMapData(emojiMapData)); - -module.exports = unicodeMapping; diff --git a/app/javascript/themes/glitch/util/emoji/emoji_utils.js b/app/javascript/themes/glitch/util/emoji/emoji_utils.js deleted file mode 100644 index dbf725c1f..000000000 --- a/app/javascript/themes/glitch/util/emoji/emoji_utils.js +++ /dev/null @@ -1,258 +0,0 @@ -// This code is largely borrowed from: -// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js - -import data from './emoji_mart_data_light'; - -const buildSearch = (data) => { - const search = []; - - let addToSearch = (strings, split) => { - if (!strings) { - return; - } - - (Array.isArray(strings) ? strings : [strings]).forEach((string) => { - (split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => { - s = s.toLowerCase(); - - if (search.indexOf(s) === -1) { - search.push(s); - } - }); - }); - }; - - addToSearch(data.short_names, true); - addToSearch(data.name, true); - addToSearch(data.keywords, false); - addToSearch(data.emoticons, false); - - return search.join(','); -}; - -const _String = String; - -const stringFromCodePoint = _String.fromCodePoint || function () { - let MAX_SIZE = 0x4000; - let codeUnits = []; - let highSurrogate; - let lowSurrogate; - let index = -1; - let length = arguments.length; - if (!length) { - return ''; - } - let result = ''; - while (++index < length) { - let codePoint = Number(arguments[index]); - if ( - !isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity` - codePoint < 0 || // not a valid Unicode code point - codePoint > 0x10FFFF || // not a valid Unicode code point - Math.floor(codePoint) !== codePoint // not an integer - ) { - throw RangeError('Invalid code point: ' + codePoint); - } - if (codePoint <= 0xFFFF) { // BMP code point - codeUnits.push(codePoint); - } else { // Astral code point; split in surrogate halves - // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae - codePoint -= 0x10000; - highSurrogate = (codePoint >> 10) + 0xD800; - lowSurrogate = (codePoint % 0x400) + 0xDC00; - codeUnits.push(highSurrogate, lowSurrogate); - } - if (index + 1 === length || codeUnits.length > MAX_SIZE) { - result += String.fromCharCode.apply(null, codeUnits); - codeUnits.length = 0; - } - } - return result; -}; - - -const _JSON = JSON; - -const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/; -const SKINS = [ - '1F3FA', '1F3FB', '1F3FC', - '1F3FD', '1F3FE', '1F3FF', -]; - -function unifiedToNative(unified) { - let unicodes = unified.split('-'), - codePoints = unicodes.map((u) => `0x${u}`); - - return stringFromCodePoint.apply(null, codePoints); -} - -function sanitize(emoji) { - let { name, short_names, skin_tone, skin_variations, emoticons, unified, custom, imageUrl } = emoji, - id = emoji.id || short_names[0], - colons = `:${id}:`; - - if (custom) { - return { - id, - name, - colons, - emoticons, - custom, - imageUrl, - }; - } - - if (skin_tone) { - colons += `:skin-tone-${skin_tone}:`; - } - - return { - id, - name, - colons, - emoticons, - unified: unified.toLowerCase(), - skin: skin_tone || (skin_variations ? 1 : null), - native: unifiedToNative(unified), - }; -} - -function getSanitizedData() { - return sanitize(getData(...arguments)); -} - -function getData(emoji, skin, set) { - let emojiData = {}; - - if (typeof emoji === 'string') { - let matches = emoji.match(COLONS_REGEX); - - if (matches) { - emoji = matches[1]; - - if (matches[2]) { - skin = parseInt(matches[2]); - } - } - - if (data.short_names.hasOwnProperty(emoji)) { - emoji = data.short_names[emoji]; - } - - if (data.emojis.hasOwnProperty(emoji)) { - emojiData = data.emojis[emoji]; - } - } else if (emoji.id) { - if (data.short_names.hasOwnProperty(emoji.id)) { - emoji.id = data.short_names[emoji.id]; - } - - if (data.emojis.hasOwnProperty(emoji.id)) { - emojiData = data.emojis[emoji.id]; - skin = skin || emoji.skin; - } - } - - if (!Object.keys(emojiData).length) { - emojiData = emoji; - emojiData.custom = true; - - if (!emojiData.search) { - emojiData.search = buildSearch(emoji); - } - } - - emojiData.emoticons = emojiData.emoticons || []; - emojiData.variations = emojiData.variations || []; - - if (emojiData.skin_variations && skin > 1 && set) { - emojiData = JSON.parse(_JSON.stringify(emojiData)); - - let skinKey = SKINS[skin - 1], - variationData = emojiData.skin_variations[skinKey]; - - if (!variationData.variations && emojiData.variations) { - delete emojiData.variations; - } - - if (variationData[`has_img_${set}`]) { - emojiData.skin_tone = skin; - - for (let k in variationData) { - let v = variationData[k]; - emojiData[k] = v; - } - } - } - - if (emojiData.variations && emojiData.variations.length) { - emojiData = JSON.parse(_JSON.stringify(emojiData)); - emojiData.unified = emojiData.variations.shift(); - } - - return emojiData; -} - -function uniq(arr) { - return arr.reduce((acc, item) => { - if (acc.indexOf(item) === -1) { - acc.push(item); - } - return acc; - }, []); -} - -function intersect(a, b) { - const uniqA = uniq(a); - const uniqB = uniq(b); - - return uniqA.filter(item => uniqB.indexOf(item) >= 0); -} - -function deepMerge(a, b) { - let o = {}; - - for (let key in a) { - let originalValue = a[key], - value = originalValue; - - if (b.hasOwnProperty(key)) { - value = b[key]; - } - - if (typeof value === 'object') { - value = deepMerge(originalValue, value); - } - - o[key] = value; - } - - return o; -} - -// https://github.com/sonicdoe/measure-scrollbar -function measureScrollbar() { - const div = document.createElement('div'); - - div.style.width = '100px'; - div.style.height = '100px'; - div.style.overflow = 'scroll'; - div.style.position = 'absolute'; - div.style.top = '-9999px'; - - document.body.appendChild(div); - const scrollbarWidth = div.offsetWidth - div.clientWidth; - document.body.removeChild(div); - - return scrollbarWidth; -} - -export { - getData, - getSanitizedData, - uniq, - intersect, - deepMerge, - unifiedToNative, - measureScrollbar, -}; diff --git a/app/javascript/themes/glitch/util/emoji/index.js b/app/javascript/themes/glitch/util/emoji/index.js deleted file mode 100644 index 8c45a58fe..000000000 --- a/app/javascript/themes/glitch/util/emoji/index.js +++ /dev/null @@ -1,95 +0,0 @@ -import { autoPlayGif } from 'themes/glitch/util/initial_state'; -import unicodeMapping from './emoji_unicode_mapping_light'; -import Trie from 'substring-trie'; - -const trie = new Trie(Object.keys(unicodeMapping)); - -const assetHost = process.env.CDN_HOST || ''; - -const emojify = (str, customEmojis = {}) => { - const tagCharsWithoutEmojis = '<&'; - const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&'; - let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0; - for (;;) { - let match, i = 0, tag; - while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) { - i += str.codePointAt(i) < 65536 ? 1 : 2; - } - let rend, replacement = ''; - if (i === str.length) { - break; - } else if (str[i] === ':') { - if (!(() => { - rend = str.indexOf(':', i + 1) + 1; - if (!rend) return false; // no pair of ':' - const lt = str.indexOf('<', i + 1); - if (!(lt === -1 || lt >= rend)) return false; // tag appeared before closing ':' - const shortname = str.slice(i, rend); - // now got a replacee as ':shortname:' - // if you want additional emoji handler, add statements below which set replacement and return true. - if (shortname in customEmojis) { - const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; - replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`; - return true; - } - return false; - })()) rend = ++i; - } else if (tag >= 0) { // <, & - rend = str.indexOf('>;'[tag], i + 1) + 1; - if (!rend) { - break; - } - if (tag === 0) { - if (invisible) { - if (str[i + 1] === '/') { // closing tag - if (!--invisible) { - tagChars = tagCharsWithEmojis; - } - } else if (str[rend - 2] !== '/') { // opening tag - invisible++; - } - } else { - if (str.startsWith('<span class="invisible">', i)) { - // avoid emojifying on invisible text - invisible = 1; - tagChars = tagCharsWithoutEmojis; - } - } - } - i = rend; - } else { // matched to unicode emoji - const { filename, shortCode } = unicodeMapping[match]; - const title = shortCode ? `:${shortCode}:` : ''; - replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`; - rend = i + match.length; - } - rtn += str.slice(0, i) + replacement; - str = str.slice(rend); - } - return rtn + str; -}; - -export default emojify; - -export const buildCustomEmojis = (customEmojis) => { - const emojis = []; - - customEmojis.forEach(emoji => { - const shortcode = emoji.get('shortcode'); - const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url'); - const name = shortcode.replace(':', ''); - - emojis.push({ - id: name, - name, - short_names: [name], - text: '', - emoticons: [], - keywords: [name], - imageUrl: url, - custom: true, - }); - }); - - return emojis; -}; diff --git a/app/javascript/themes/glitch/util/emoji/unicode_to_filename.js b/app/javascript/themes/glitch/util/emoji/unicode_to_filename.js deleted file mode 100644 index c75c4cd7d..000000000 --- a/app/javascript/themes/glitch/util/emoji/unicode_to_filename.js +++ /dev/null @@ -1,26 +0,0 @@ -// taken from: -// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866 -exports.unicodeToFilename = (str) => { - let result = ''; - let charCode = 0; - let p = 0; - let i = 0; - while (i < str.length) { - charCode = str.charCodeAt(i++); - if (p) { - if (result.length > 0) { - result += '-'; - } - result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16); - p = 0; - } else if (0xD800 <= charCode && charCode <= 0xDBFF) { - p = charCode; - } else { - if (result.length > 0) { - result += '-'; - } - result += charCode.toString(16); - } - } - return result; -}; diff --git a/app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js b/app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js deleted file mode 100644 index 808ac197e..000000000 --- a/app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js +++ /dev/null @@ -1,17 +0,0 @@ -function padLeft(str, num) { - while (str.length < num) { - str = '0' + str; - } - return str; -} - -exports.unicodeToUnifiedName = (str) => { - let output = ''; - for (let i = 0; i < str.length; i += 2) { - if (i > 0) { - output += '-'; - } - output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4); - } - return output; -}; diff --git a/app/javascript/themes/glitch/util/extra_polyfills.js b/app/javascript/themes/glitch/util/extra_polyfills.js deleted file mode 100644 index 3acc55abd..000000000 --- a/app/javascript/themes/glitch/util/extra_polyfills.js +++ /dev/null @@ -1,5 +0,0 @@ -import 'intersection-observer'; -import 'requestidlecallback'; -import objectFitImages from 'object-fit-images'; - -objectFitImages(); diff --git a/app/javascript/themes/glitch/util/fullscreen.js b/app/javascript/themes/glitch/util/fullscreen.js deleted file mode 100644 index cf5d0cf98..000000000 --- a/app/javascript/themes/glitch/util/fullscreen.js +++ /dev/null @@ -1,46 +0,0 @@ -// APIs for normalizing fullscreen operations. Note that Edge uses -// the WebKit-prefixed APIs currently (as of Edge 16). - -export const isFullscreen = () => document.fullscreenElement || - document.webkitFullscreenElement || - document.mozFullScreenElement; - -export const exitFullscreen = () => { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } -}; - -export const requestFullscreen = el => { - if (el.requestFullscreen) { - el.requestFullscreen(); - } else if (el.webkitRequestFullscreen) { - el.webkitRequestFullscreen(); - } else if (el.mozRequestFullScreen) { - el.mozRequestFullScreen(); - } -}; - -export const attachFullscreenListener = (listener) => { - if ('onfullscreenchange' in document) { - document.addEventListener('fullscreenchange', listener); - } else if ('onwebkitfullscreenchange' in document) { - document.addEventListener('webkitfullscreenchange', listener); - } else if ('onmozfullscreenchange' in document) { - document.addEventListener('mozfullscreenchange', listener); - } -}; - -export const detachFullscreenListener = (listener) => { - if ('onfullscreenchange' in document) { - document.removeEventListener('fullscreenchange', listener); - } else if ('onwebkitfullscreenchange' in document) { - document.removeEventListener('webkitfullscreenchange', listener); - } else if ('onmozfullscreenchange' in document) { - document.removeEventListener('mozfullscreenchange', listener); - } -}; diff --git a/app/javascript/themes/glitch/util/get_rect_from_entry.js b/app/javascript/themes/glitch/util/get_rect_from_entry.js deleted file mode 100644 index c266cd7dc..000000000 --- a/app/javascript/themes/glitch/util/get_rect_from_entry.js +++ /dev/null @@ -1,21 +0,0 @@ - -// Get the bounding client rect from an IntersectionObserver entry. -// This is to work around a bug in Chrome: https://crbug.com/737228 - -let hasBoundingRectBug; - -function getRectFromEntry(entry) { - if (typeof hasBoundingRectBug !== 'boolean') { - const boundingRect = entry.target.getBoundingClientRect(); - const observerRect = entry.boundingClientRect; - hasBoundingRectBug = boundingRect.height !== observerRect.height || - boundingRect.top !== observerRect.top || - boundingRect.width !== observerRect.width || - boundingRect.bottom !== observerRect.bottom || - boundingRect.left !== observerRect.left || - boundingRect.right !== observerRect.right; - } - return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect; -} - -export default getRectFromEntry; diff --git a/app/javascript/themes/glitch/util/initial_state.js b/app/javascript/themes/glitch/util/initial_state.js deleted file mode 100644 index ef5d8b0ef..000000000 --- a/app/javascript/themes/glitch/util/initial_state.js +++ /dev/null @@ -1,21 +0,0 @@ -const element = document.getElementById('initial-state'); -const initialState = element && function () { - const result = JSON.parse(element.textContent); - try { - result.local_settings = JSON.parse(localStorage.getItem('mastodon-settings')); - } catch (e) { - result.local_settings = {}; - } - return result; -}(); - -const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop]; - -export const reduceMotion = getMeta('reduce_motion'); -export const autoPlayGif = getMeta('auto_play_gif'); -export const unfollowModal = getMeta('unfollow_modal'); -export const boostModal = getMeta('boost_modal'); -export const deleteModal = getMeta('delete_modal'); -export const me = getMeta('me'); - -export default initialState; diff --git a/app/javascript/themes/glitch/util/intersection_observer_wrapper.js b/app/javascript/themes/glitch/util/intersection_observer_wrapper.js deleted file mode 100644 index 2b24c6583..000000000 --- a/app/javascript/themes/glitch/util/intersection_observer_wrapper.js +++ /dev/null @@ -1,57 +0,0 @@ -// Wrapper for IntersectionObserver in order to make working with it -// a bit easier. We also follow this performance advice: -// "If you need to observe multiple elements, it is both possible and -// advised to observe multiple elements using the same IntersectionObserver -// instance by calling observe() multiple times." -// https://developers.google.com/web/updates/2016/04/intersectionobserver - -class IntersectionObserverWrapper { - - callbacks = {}; - observerBacklog = []; - observer = null; - - connect (options) { - const onIntersection = (entries) => { - entries.forEach(entry => { - const id = entry.target.getAttribute('data-id'); - if (this.callbacks[id]) { - this.callbacks[id](entry); - } - }); - }; - - this.observer = new IntersectionObserver(onIntersection, options); - this.observerBacklog.forEach(([ id, node, callback ]) => { - this.observe(id, node, callback); - }); - this.observerBacklog = null; - } - - observe (id, node, callback) { - if (!this.observer) { - this.observerBacklog.push([ id, node, callback ]); - } else { - this.callbacks[id] = callback; - this.observer.observe(node); - } - } - - unobserve (id, node) { - if (this.observer) { - delete this.callbacks[id]; - this.observer.unobserve(node); - } - } - - disconnect () { - if (this.observer) { - this.callbacks = {}; - this.observer.disconnect(); - this.observer = null; - } - } - -} - -export default IntersectionObserverWrapper; diff --git a/app/javascript/themes/glitch/util/is_mobile.js b/app/javascript/themes/glitch/util/is_mobile.js deleted file mode 100644 index 80e8e0a8a..000000000 --- a/app/javascript/themes/glitch/util/is_mobile.js +++ /dev/null @@ -1,34 +0,0 @@ -import detectPassiveEvents from 'detect-passive-events'; - -const LAYOUT_BREAKPOINT = 630; - -export function isMobile(width, columns) { - switch (columns) { - case 'multiple': - return false; - case 'single': - return true; - default: - return width <= LAYOUT_BREAKPOINT; - } -}; - -const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; - -let userTouching = false; -let listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; - -function touchListener() { - userTouching = true; - window.removeEventListener('touchstart', touchListener, listenerOptions); -} - -window.addEventListener('touchstart', touchListener, listenerOptions); - -export function isUserTouching() { - return userTouching; -} - -export function isIOS() { - return iOS; -}; diff --git a/app/javascript/themes/glitch/util/link_header.js b/app/javascript/themes/glitch/util/link_header.js deleted file mode 100644 index a3e7ccf1c..000000000 --- a/app/javascript/themes/glitch/util/link_header.js +++ /dev/null @@ -1,33 +0,0 @@ -import Link from 'http-link-header'; -import querystring from 'querystring'; - -Link.parseAttrs = (link, parts) => { - let match = null; - let attr = ''; - let value = ''; - let attrs = ''; - - let uriAttrs = /<(.*)>;\s*(.*)/gi.exec(parts); - - if(uriAttrs) { - attrs = uriAttrs[2]; - link = Link.parseParams(link, uriAttrs[1]); - } - - while(match = Link.attrPattern.exec(attrs)) { // eslint-disable-line no-cond-assign - attr = match[1].toLowerCase(); - value = match[4] || match[3] || match[2]; - - if( /\*$/.test(attr)) { - Link.setAttr(link, attr, Link.parseExtendedValue(value)); - } else if(/%/.test(value)) { - Link.setAttr(link, attr, querystring.decode(value)); - } else { - Link.setAttr(link, attr, value); - } - } - - return link; -}; - -export default Link; diff --git a/app/javascript/themes/glitch/util/load_polyfills.js b/app/javascript/themes/glitch/util/load_polyfills.js deleted file mode 100644 index 8927b7358..000000000 --- a/app/javascript/themes/glitch/util/load_polyfills.js +++ /dev/null @@ -1,39 +0,0 @@ -// Convenience function to load polyfills and return a promise when it's done. -// If there are no polyfills, then this is just Promise.resolve() which means -// it will execute in the same tick of the event loop (i.e. near-instant). - -function importBasePolyfills() { - return import(/* webpackChunkName: "base_polyfills" */ './base_polyfills'); -} - -function importExtraPolyfills() { - return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills'); -} - -function loadPolyfills() { - const needsBasePolyfills = !( - window.Intl && - Object.assign && - Number.isNaN && - window.Symbol && - Array.prototype.includes - ); - - // Latest version of Firefox and Safari do not have IntersectionObserver. - // Edge does not have requestIdleCallback and object-fit CSS property. - // This avoids shipping them all the polyfills. - const needsExtraPolyfills = !( - window.IntersectionObserver && - window.IntersectionObserverEntry && - 'isIntersecting' in IntersectionObserverEntry.prototype && - window.requestIdleCallback && - 'object-fit' in (new Image()).style - ); - - return Promise.all([ - needsBasePolyfills && importBasePolyfills(), - needsExtraPolyfills && importExtraPolyfills(), - ]); -} - -export default loadPolyfills; diff --git a/app/javascript/themes/glitch/util/main.js b/app/javascript/themes/glitch/util/main.js deleted file mode 100644 index c10a64ded..000000000 --- a/app/javascript/themes/glitch/util/main.js +++ /dev/null @@ -1,39 +0,0 @@ -import * as WebPushSubscription from './web_push_subscription'; -import Mastodon from 'themes/glitch/containers/mastodon'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import ready from './ready'; - -const perf = require('./performance'); - -function main() { - perf.start('main()'); - - if (window.history && history.replaceState) { - const { pathname, search, hash } = window.location; - const path = pathname + search + hash; - if (!(/^\/web[$/]/).test(path)) { - history.replaceState(null, document.title, `/web${path}`); - } - } - - ready(() => { - const mountNode = document.getElementById('mastodon'); - const props = JSON.parse(mountNode.getAttribute('data-props')); - - ReactDOM.render(<Mastodon {...props} />, mountNode); - if (process.env.NODE_ENV === 'production') { - // avoid offline in dev mode because it's harder to debug - require('offline-plugin/runtime').install(); - WebPushSubscription.register(); - } - perf.stop('main()'); - - // remember the initial URL - if (window.history && typeof window._mastoInitialHistoryLen === 'undefined') { - window._mastoInitialHistoryLen = window.history.length; - } - }); -} - -export default main; diff --git a/app/javascript/themes/glitch/util/optional_motion.js b/app/javascript/themes/glitch/util/optional_motion.js deleted file mode 100644 index b8a57b22f..000000000 --- a/app/javascript/themes/glitch/util/optional_motion.js +++ /dev/null @@ -1,5 +0,0 @@ -import { reduceMotion } from 'themes/glitch/util/initial_state'; -import ReducedMotion from './reduced_motion'; -import Motion from 'react-motion/lib/Motion'; - -export default reduceMotion ? ReducedMotion : Motion; diff --git a/app/javascript/themes/glitch/util/performance.js b/app/javascript/themes/glitch/util/performance.js deleted file mode 100644 index 450a90626..000000000 --- a/app/javascript/themes/glitch/util/performance.js +++ /dev/null @@ -1,31 +0,0 @@ -// -// Tools for performance debugging, only enabled in development mode. -// Open up Chrome Dev Tools, then Timeline, then User Timing to see output. -// Also see config/webpack/loaders/mark.js for the webpack loader marks. -// - -let marky; - -if (process.env.NODE_ENV === 'development') { - if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) { - // Increase Firefox's performance entry limit; otherwise it's capped to 150. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135 - performance.setResourceTimingBufferSize(Infinity); - } - marky = require('marky'); - // allows us to easily do e.g. ReactPerf.printWasted() while debugging - //window.ReactPerf = require('react-addons-perf'); - //window.ReactPerf.start(); -} - -export function start(name) { - if (process.env.NODE_ENV === 'development') { - marky.mark(name); - } -} - -export function stop(name) { - if (process.env.NODE_ENV === 'development') { - marky.stop(name); - } -} diff --git a/app/javascript/themes/glitch/util/react_router_helpers.js b/app/javascript/themes/glitch/util/react_router_helpers.js deleted file mode 100644 index c02fb5247..000000000 --- a/app/javascript/themes/glitch/util/react_router_helpers.js +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Switch, Route } from 'react-router-dom'; - -import ColumnLoading from 'themes/glitch/features/ui/components/column_loading'; -import BundleColumnError from 'themes/glitch/features/ui/components/bundle_column_error'; -import BundleContainer from 'themes/glitch/features/ui/containers/bundle_container'; - -// Small wrapper to pass multiColumn to the route components -export class WrappedSwitch extends React.PureComponent { - - render () { - const { multiColumn, children } = this.props; - - return ( - <Switch> - {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} - </Switch> - ); - } - -} - -WrappedSwitch.propTypes = { - multiColumn: PropTypes.bool, - children: PropTypes.node, -}; - -// Small Wraper to extract the params from the route and pass -// them to the rendered component, together with the content to -// be rendered inside (the children) -export class WrappedRoute extends React.Component { - - static propTypes = { - component: PropTypes.func.isRequired, - content: PropTypes.node, - multiColumn: PropTypes.bool, - } - - renderComponent = ({ match }) => { - const { component, content, multiColumn } = this.props; - - return ( - <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}> - {Component => <Component params={match.params} multiColumn={multiColumn}>{content}</Component>} - </BundleContainer> - ); - } - - renderLoading = () => { - return <ColumnLoading />; - } - - renderError = (props) => { - return <BundleColumnError {...props} />; - } - - render () { - const { component: Component, content, ...rest } = this.props; - - return <Route {...rest} render={this.renderComponent} />; - } - -} diff --git a/app/javascript/themes/glitch/util/ready.js b/app/javascript/themes/glitch/util/ready.js deleted file mode 100644 index dd543910b..000000000 --- a/app/javascript/themes/glitch/util/ready.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function ready(loaded) { - if (['interactive', 'complete'].includes(document.readyState)) { - loaded(); - } else { - document.addEventListener('DOMContentLoaded', loaded); - } -} diff --git a/app/javascript/themes/glitch/util/reduced_motion.js b/app/javascript/themes/glitch/util/reduced_motion.js deleted file mode 100644 index 95519042b..000000000 --- a/app/javascript/themes/glitch/util/reduced_motion.js +++ /dev/null @@ -1,44 +0,0 @@ -// Like react-motion's Motion, but reduces all animations to cross-fades -// for the benefit of users with motion sickness. -import React from 'react'; -import Motion from 'react-motion/lib/Motion'; -import PropTypes from 'prop-types'; - -const stylesToKeep = ['opacity', 'backgroundOpacity']; - -const extractValue = (value) => { - // This is either an object with a "val" property or it's a number - return (typeof value === 'object' && value && 'val' in value) ? value.val : value; -}; - -class ReducedMotion extends React.Component { - - static propTypes = { - defaultStyle: PropTypes.object, - style: PropTypes.object, - children: PropTypes.func, - } - - render() { - - const { style, defaultStyle, children } = this.props; - - Object.keys(style).forEach(key => { - if (stylesToKeep.includes(key)) { - return; - } - // If it's setting an x or height or scale or some other value, we need - // to preserve the end-state value without actually animating it - style[key] = defaultStyle[key] = extractValue(style[key]); - }); - - return ( - <Motion style={style} defaultStyle={defaultStyle}> - {children} - </Motion> - ); - } - -} - -export default ReducedMotion; diff --git a/app/javascript/themes/glitch/util/rtl.js b/app/javascript/themes/glitch/util/rtl.js deleted file mode 100644 index 00870a15d..000000000 --- a/app/javascript/themes/glitch/util/rtl.js +++ /dev/null @@ -1,31 +0,0 @@ -// U+0590 to U+05FF - Hebrew -// U+0600 to U+06FF - Arabic -// U+0700 to U+074F - Syriac -// U+0750 to U+077F - Arabic Supplement -// U+0780 to U+07BF - Thaana -// U+07C0 to U+07FF - N'Ko -// U+0800 to U+083F - Samaritan -// U+08A0 to U+08FF - Arabic Extended-A -// U+FB1D to U+FB4F - Hebrew presentation forms -// U+FB50 to U+FDFF - Arabic presentation forms A -// U+FE70 to U+FEFF - Arabic presentation forms B - -const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg; - -export function isRtl(text) { - if (text.length === 0) { - return false; - } - - text = text.replace(/(?:^|[^\/\w])@([a-z0-9_]+(@[a-z0-9\.\-]+)?)/ig, ''); - text = text.replace(/(?:^|[^\/\w])#([\S]+)/ig, ''); - text = text.replace(/\s+/g, ''); - - const matches = text.match(rtlChars); - - if (!matches) { - return false; - } - - return matches.length / text.length > 0.3; -}; diff --git a/app/javascript/themes/glitch/util/schedule_idle_task.js b/app/javascript/themes/glitch/util/schedule_idle_task.js deleted file mode 100644 index b04d4a8ee..000000000 --- a/app/javascript/themes/glitch/util/schedule_idle_task.js +++ /dev/null @@ -1,29 +0,0 @@ -// Wrapper to call requestIdleCallback() to schedule low-priority work. -// See https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API -// for a good breakdown of the concepts behind this. - -import Queue from 'tiny-queue'; - -const taskQueue = new Queue(); -let runningRequestIdleCallback = false; - -function runTasks(deadline) { - while (taskQueue.length && deadline.timeRemaining() > 0) { - taskQueue.shift()(); - } - if (taskQueue.length) { - requestIdleCallback(runTasks); - } else { - runningRequestIdleCallback = false; - } -} - -function scheduleIdleTask(task) { - taskQueue.push(task); - if (!runningRequestIdleCallback) { - runningRequestIdleCallback = true; - requestIdleCallback(runTasks); - } -} - -export default scheduleIdleTask; diff --git a/app/javascript/themes/glitch/util/scroll.js b/app/javascript/themes/glitch/util/scroll.js deleted file mode 100644 index 2af07e0fb..000000000 --- a/app/javascript/themes/glitch/util/scroll.js +++ /dev/null @@ -1,30 +0,0 @@ -const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; - -const scroll = (node, key, target) => { - const startTime = Date.now(); - const offset = node[key]; - const gap = target - offset; - const duration = 1000; - let interrupt = false; - - const step = () => { - const elapsed = Date.now() - startTime; - const percentage = elapsed / duration; - - if (percentage > 1 || interrupt) { - return; - } - - node[key] = easingOutQuint(0, elapsed, offset, gap, duration); - requestAnimationFrame(step); - }; - - step(); - - return () => { - interrupt = true; - }; -}; - -export const scrollRight = (node, position) => scroll(node, 'scrollLeft', position); -export const scrollTop = (node) => scroll(node, 'scrollTop', 0); diff --git a/app/javascript/themes/glitch/util/stream.js b/app/javascript/themes/glitch/util/stream.js deleted file mode 100644 index 36c68ffc5..000000000 --- a/app/javascript/themes/glitch/util/stream.js +++ /dev/null @@ -1,73 +0,0 @@ -import WebSocketClient from 'websocket.js'; - -export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) { - return (dispatch, getState) => { - const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); - const accessToken = getState().getIn(['meta', 'access_token']); - const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState); - let polling = null; - - const setupPolling = () => { - polling = setInterval(() => { - pollingRefresh(dispatch); - }, 20000); - }; - - const clearPolling = () => { - if (polling) { - clearInterval(polling); - polling = null; - } - }; - - const subscription = getStream(streamingAPIBaseURL, accessToken, path, { - connected () { - if (pollingRefresh) { - clearPolling(); - } - onConnect(); - }, - - disconnected () { - if (pollingRefresh) { - setupPolling(); - } - onDisconnect(); - }, - - received (data) { - onReceive(data); - }, - - reconnected () { - if (pollingRefresh) { - clearPolling(); - pollingRefresh(dispatch); - } - onConnect(); - }, - - }); - - const disconnect = () => { - if (subscription) { - subscription.close(); - } - clearPolling(); - }; - - return disconnect; - }; -} - - -export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) { - const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`); - - ws.onopen = connected; - ws.onmessage = e => received(JSON.parse(e.data)); - ws.onclose = disconnected; - ws.onreconnect = reconnected; - - return ws; -}; diff --git a/app/javascript/themes/glitch/util/url_regex.js b/app/javascript/themes/glitch/util/url_regex.js deleted file mode 100644 index e676d1879..000000000 --- a/app/javascript/themes/glitch/util/url_regex.js +++ /dev/null @@ -1,196 +0,0 @@ -const regexen = {}; - -const regexSupplant = function(regex, flags) { - flags = flags || ''; - if (typeof regex !== 'string') { - if (regex.global && flags.indexOf('g') < 0) { - flags += 'g'; - } - if (regex.ignoreCase && flags.indexOf('i') < 0) { - flags += 'i'; - } - if (regex.multiline && flags.indexOf('m') < 0) { - flags += 'm'; - } - - regex = regex.source; - } - return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { - var newRegex = regexen[name] || ''; - if (typeof newRegex !== 'string') { - newRegex = newRegex.source; - } - return newRegex; - }), flags); -}; - -const stringSupplant = function(str, values) { - return str.replace(/#\{(\w+)\}/g, function(match, name) { - return values[name] || ''; - }); -}; - -export const urlRegex = (function() { - regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/; - regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/; - regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/; - regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@@$###{invalid_chars_group}]|^)/); - regexen.invalidDomainChars = stringSupplant('#{punct}#{spaces_group}#{invalid_chars_group}', regexen); - regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/); - regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/); - regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/); - regexen.validGTLD = regexSupplant(RegExp( - '(?:(?:' + - '삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' + - '政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' + - 'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' + - 'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' + - 'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' + - 'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' + - 'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' + - 'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' + - 'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' + - 'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' + - 'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' + - 'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' + - 'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' + - 'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' + - 'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' + - 'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' + - 'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' + - 'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' + - 'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' + - 'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' + - 'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' + - 'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' + - 'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' + - 'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' + - 'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' + - 'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' + - 'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' + - 'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' + - 'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' + - 'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' + - 'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' + - 'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' + - 'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' + - 'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' + - 'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' + - 'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' + - 'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' + - 'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' + - 'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' + - 'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' + - 'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' + - 'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' + - 'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' + - 'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' + - 'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' + - 'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' + - 'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' + - 'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' + - 'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' + - 'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' + - 'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' + - 'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' + - 'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' + - 'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' + - 'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' + - 'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' + - 'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' + - 'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' + - 'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' + - 'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' + - 'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' + - 'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' + - 'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' + - 'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' + - 'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' + - 'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' + - 'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' + - 'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' + - 'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' + - 'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' + - 'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' + - 'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' + - 'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' + - 'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' + - 'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' + - 'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' + - 'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' + - 'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' + - 'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' + - 'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' + - 'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' + - 'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' + - 'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' + - 'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' + - 'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' + - 'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' + - 'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' + - 'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' + - ')(?=[^0-9a-zA-Z@]|$))')); - regexen.validCCTLD = regexSupplant(RegExp( - '(?:(?:' + - '한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' + - 'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' + - 'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' + - 'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' + - 'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' + - 're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' + - 'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' + - 'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' + - 'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' + - 'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' + - 'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' + - ')(?=[^0-9a-zA-Z@]|$))')); - regexen.validPunycode = /(?:xn--[0-9a-z]+)/; - regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/; - regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/); - regexen.validPortNumber = /[0-9]+/; - regexen.pd = /\u002d\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a\u2e40\u301c\u3030\u30a0\ufe31\ufe58\ufe63\uff0d/; - regexen.validGeneralUrlPathChars = regexSupplant(/[^#{spaces_group}\(\)\?]/i); - // Allow URL paths to contain up to two nested levels of balanced parens - // 1. Used in Wikipedia URLs like /Primer_(film) - // 2. Used in IIS sessions like /S(dfd346)/ - // 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/ - regexen.validUrlBalancedParens = regexSupplant( - '\\(' + - '(?:' + - '#{validGeneralUrlPathChars}+' + - '|' + - // allow one nested level of balanced parentheses - '(?:' + - '#{validGeneralUrlPathChars}*' + - '\\(' + - '#{validGeneralUrlPathChars}+' + - '\\)' + - '#{validGeneralUrlPathChars}*' + - ')' + - ')' + - '\\)' - , 'i'); - // Valid end-of-path chracters (so /foo. does not gobble the period). - // 1. Allow =&# for empty URL parameters and other URL-join artifacts - regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i); - // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/ - regexen.validUrlPath = regexSupplant('(?:' + - '(?:' + - '#{validGeneralUrlPathChars}*' + - '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' + - '#{validUrlPathEndingChars}'+ - ')|(?:@#{validGeneralUrlPathChars}+\/)'+ - ')', 'i'); - regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i; - regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i; - regexen.validUrl = regexSupplant( - '(' + // $1 URL - '(https?:\\/\\/)' + // $2 Protocol - '(#{validDomain})' + // $3 Domain(s) - '(?::(#{validPortNumber}))?' + // $4 Port number (optional) - '(\\/#{validUrlPath}*)?' + // $5 URL Path - '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $6 Query String - ')' - , 'gi'); - return regexen.validUrl; -}()); diff --git a/app/javascript/themes/glitch/util/uuid.js b/app/javascript/themes/glitch/util/uuid.js deleted file mode 100644 index be1899305..000000000 --- a/app/javascript/themes/glitch/util/uuid.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function uuid(a) { - return a ? (a^Math.random() * 16 >> a / 4).toString(16) : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid); -}; diff --git a/app/javascript/themes/glitch/util/web_push_subscription.js b/app/javascript/themes/glitch/util/web_push_subscription.js deleted file mode 100644 index 70b26105b..000000000 --- a/app/javascript/themes/glitch/util/web_push_subscription.js +++ /dev/null @@ -1,105 +0,0 @@ -import axios from 'axios'; -import { store } from 'themes/glitch/containers/mastodon'; -import { setBrowserSupport, setSubscription, clearSubscription } from 'themes/glitch/actions/push_notifications'; - -// Taken from https://www.npmjs.com/package/web-push -const urlBase64ToUint8Array = (base64String) => { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding) - .replace(/\-/g, '+') - .replace(/_/g, '/'); - - const rawData = window.atob(base64); - const outputArray = new Uint8Array(rawData.length); - - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); - } - return outputArray; -}; - -const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content'); - -const getRegistration = () => navigator.serviceWorker.ready; - -const getPushSubscription = (registration) => - registration.pushManager.getSubscription() - .then(subscription => ({ registration, subscription })); - -const subscribe = (registration) => - registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()), - }); - -const unsubscribe = ({ registration, subscription }) => - subscription ? subscription.unsubscribe().then(() => registration) : registration; - -const sendSubscriptionToBackend = (subscription) => - axios.post('/api/web/push_subscriptions', { - subscription, - }).then(response => response.data); - -// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload -const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); - -export function register () { - store.dispatch(setBrowserSupport(supportsPushNotifications)); - - if (supportsPushNotifications) { - if (!getApplicationServerKey()) { - console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); - return; - } - - getRegistration() - .then(getPushSubscription) - .then(({ registration, subscription }) => { - if (subscription !== null) { - // We have a subscription, check if it is still valid - const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString(); - const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString(); - const serverEndpoint = store.getState().getIn(['push_notifications', 'subscription', 'endpoint']); - - // If the VAPID public key did not change and the endpoint corresponds - // to the endpoint saved in the backend, the subscription is valid - if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { - return subscription; - } else { - // Something went wrong, try to subscribe again - return unsubscribe({ registration, subscription }).then(subscribe).then(sendSubscriptionToBackend); - } - } - - // No subscription, try to subscribe - return subscribe(registration).then(sendSubscriptionToBackend); - }) - .then(subscription => { - // If we got a PushSubscription (and not a subscription object from the backend) - // it means that the backend subscription is valid (and was set during hydration) - if (!(subscription instanceof PushSubscription)) { - store.dispatch(setSubscription(subscription)); - } - }) - .catch(error => { - if (error.code === 20 && error.name === 'AbortError') { - console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); - } else if (error.code === 5 && error.name === 'InvalidCharacterError') { - console.error('The VAPID public key seems to be invalid:', getApplicationServerKey()); - } - - // Clear alerts and hide UI settings - store.dispatch(clearSubscription()); - - try { - getRegistration() - .then(getPushSubscription) - .then(unsubscribe); - } catch (e) { - - } - }); - } else { - console.warn('Your browser does not support Web Push Notifications.'); - } -} |